mirror of
https://github.com/Troplo/Colubrina.git
synced 2024-11-22 19:27:55 +11:00
1.0.5
This commit is contained in:
parent
3562843c10
commit
52d82acde3
12 changed files with 1362 additions and 1334 deletions
|
@ -11,7 +11,7 @@ Colubrina is a simple self-hostable chatting platform written in Vue, and Vuetif
|
||||||
- [x] Authentication
|
- [x] Authentication
|
||||||
- [x] Admin panel
|
- [x] Admin panel
|
||||||
- [x] CLI
|
- [x] CLI
|
||||||
- [ ] Scroll up to see more messages/jump to searched message
|
- [x] (partially complete) Scroll up to see more messages/jump to searched message
|
||||||
- [x] User profile cards
|
- [x] User profile cards
|
||||||
- [x] Group creation and modification
|
- [x] Group creation and modification
|
||||||
- [x] Direct message groups
|
- [x] Direct message groups
|
||||||
|
|
|
@ -21,6 +21,7 @@ app.use("/api/v1/admin", require("./routes/admin.js"))
|
||||||
app.use("/usercontent", require("./routes/usercontent.js"))
|
app.use("/usercontent", require("./routes/usercontent.js"))
|
||||||
app.use("/api/v1/usercontent", require("./routes/usercontent.js"))
|
app.use("/api/v1/usercontent", require("./routes/usercontent.js"))
|
||||||
app.use("/api/v1/mediaproxy", require("./routes/mediaproxy.js"))
|
app.use("/api/v1/mediaproxy", require("./routes/mediaproxy.js"))
|
||||||
|
app.use("/api/v1/associations", require("./routes/associations.js"))
|
||||||
app.get("/api/v1/state", async (req, res) => {
|
app.get("/api/v1/state", async (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
release: process.env.RELEASE,
|
release: process.env.RELEASE,
|
||||||
|
|
|
@ -98,15 +98,18 @@ module.exports = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
socket.on("disconnect", async function () {
|
socket.on("disconnect", async function () {
|
||||||
friends.forEach((friend) => {
|
const clients = io.sockets.adapter.rooms.get(user.id) || new Set()
|
||||||
io.to(friend.friendId).emit("userStatus", {
|
if (!clients.size || clients.size === 0) {
|
||||||
userId: user.id,
|
friends.forEach((friend) => {
|
||||||
|
io.to(friend.friendId).emit("userStatus", {
|
||||||
|
userId: user.id,
|
||||||
|
status: "offline"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
await user.update({
|
||||||
status: "offline"
|
status: "offline"
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
await user.update({
|
|
||||||
status: "offline"
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
socket.join(-1)
|
socket.join(-1)
|
||||||
|
|
514
backend/routes/associations.js
Normal file
514
backend/routes/associations.js
Normal file
|
@ -0,0 +1,514 @@
|
||||||
|
const express = require("express")
|
||||||
|
const router = express.Router()
|
||||||
|
const Errors = require("../lib/errors.js")
|
||||||
|
const auth = require("../lib/authorize.js")
|
||||||
|
const { User, Message, ChatAssociation, Chat, Attachment, Friend} = require("../models")
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
"/association/:id/:associationId",
|
||||||
|
auth,
|
||||||
|
async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const io = req.app.get("io")
|
||||||
|
const chat = await ChatAssociation.findOne({
|
||||||
|
where: {
|
||||||
|
id: req.params.id,
|
||||||
|
userId: req.user.id,
|
||||||
|
rank: "admin"
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Chat,
|
||||||
|
as: "chat",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "users",
|
||||||
|
attributes: ["id", "username", "createdAt", "updatedAt"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
const association = await ChatAssociation.findOne({
|
||||||
|
where: {
|
||||||
|
id: req.params.associationId,
|
||||||
|
chatId: chat.chat.id
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "user",
|
||||||
|
attributes: ["id", "username", "createdAt", "updatedAt"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
if (!chat) {
|
||||||
|
throw Errors.chatNotFoundOrNotAdmin
|
||||||
|
}
|
||||||
|
if (!association) {
|
||||||
|
throw Errors.chatNotFoundOrNotAdmin
|
||||||
|
}
|
||||||
|
if(association.chat)
|
||||||
|
await association.destroy()
|
||||||
|
res.sendStatus(204)
|
||||||
|
const message = await Message.create({
|
||||||
|
userId: 0,
|
||||||
|
chatId: chat.chat.id,
|
||||||
|
content: `${association.user.username} has been removed by ${req.user.username}.`,
|
||||||
|
type: "leave"
|
||||||
|
})
|
||||||
|
const associations = await ChatAssociation.findAll({
|
||||||
|
where: {
|
||||||
|
chatId: chat.chat.id
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "user",
|
||||||
|
attributes: [
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
"avatar",
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
const messageLookup = await Message.findOne({
|
||||||
|
where: {
|
||||||
|
id: message.id
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Attachment,
|
||||||
|
as: "attachments"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Message,
|
||||||
|
as: "reply",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "user",
|
||||||
|
attributes: [
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
"avatar",
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Chat,
|
||||||
|
as: "chat",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "users",
|
||||||
|
attributes: [
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
"avatar",
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "user",
|
||||||
|
attributes: [
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
"avatar",
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
associations.forEach((association) => {
|
||||||
|
io.to(association.userId).emit("message", {
|
||||||
|
...messageLookup.dataValues,
|
||||||
|
associationId: association.id,
|
||||||
|
keyId: `${message.id}-${message.updatedAt.toISOString()}`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
router.put("/association/:id/:associationId", auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const chat = await ChatAssociation.findOne({
|
||||||
|
where: {
|
||||||
|
id: req.params.id,
|
||||||
|
userId: req.user.id,
|
||||||
|
rank: "admin"
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Chat,
|
||||||
|
as: "chat",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "users",
|
||||||
|
attributes: ["id", "username", "createdAt", "updatedAt"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
const association = await ChatAssociation.findOne({
|
||||||
|
where: {
|
||||||
|
id: req.params.associationId,
|
||||||
|
chatId: chat.chat.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!chat) {
|
||||||
|
throw Errors.chatNotFoundOrNotAdmin
|
||||||
|
}
|
||||||
|
if (!association) {
|
||||||
|
throw Errors.chatNotFoundOrNotAdmin
|
||||||
|
}
|
||||||
|
if (association.rank === "admin") {
|
||||||
|
throw Errors.chatNotFoundOrNotAdmin
|
||||||
|
}
|
||||||
|
await association.update({
|
||||||
|
rank: req.body.rank
|
||||||
|
})
|
||||||
|
res.sendStatus(204)
|
||||||
|
} catch (err) {
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.post("/association/:id", auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const io = req.app.get("io")
|
||||||
|
const chat = await ChatAssociation.findOne({
|
||||||
|
where: {
|
||||||
|
userId: req.user.id,
|
||||||
|
chatId: req.params.id,
|
||||||
|
rank: "admin"
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Chat,
|
||||||
|
as: "chat",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "users",
|
||||||
|
attributes: ["id", "username", "createdAt", "updatedAt"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
if (chat) {
|
||||||
|
if (req.body.users.length > 10) {
|
||||||
|
throw Errors.invalidParameter(
|
||||||
|
"User",
|
||||||
|
"The maximum number of users is 10"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!req.body.users.length) {
|
||||||
|
throw Errors.invalidParameter(
|
||||||
|
"User",
|
||||||
|
"You need at least 1 user to create a chat"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (req.body.users.includes(req.user.id)) {
|
||||||
|
throw Errors.invalidParameter(
|
||||||
|
"User",
|
||||||
|
"You cannot create a DM with yourself"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const friends = await Friend.findAll({
|
||||||
|
where: {
|
||||||
|
userId: req.user.id,
|
||||||
|
friendId: req.body.users,
|
||||||
|
status: "accepted"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (friends.length !== req.body.users.length) {
|
||||||
|
throw Errors.invalidParameter(
|
||||||
|
"User",
|
||||||
|
"You are not friends with this user"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const users = await ChatAssociation.findAll({
|
||||||
|
where: {
|
||||||
|
userId: req.body.users,
|
||||||
|
chatId: req.params.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (users.length > 0) {
|
||||||
|
throw Errors.invalidParameter(
|
||||||
|
"User",
|
||||||
|
"One or more users are already in this chat"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const associations = await ChatAssociation.findAll({
|
||||||
|
where: {
|
||||||
|
chatId: chat.chatId
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "user",
|
||||||
|
attributes: [
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
"avatar",
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
for (let i = 0; i < req.body.users.length; i++) {
|
||||||
|
const c1 = await ChatAssociation.create({
|
||||||
|
chatId: chat.chat.id,
|
||||||
|
userId: req.body.users[i],
|
||||||
|
rank: "member"
|
||||||
|
})
|
||||||
|
const association = await ChatAssociation.findOne({
|
||||||
|
where: {
|
||||||
|
id: c1.id
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Chat,
|
||||||
|
as: "chat",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "users",
|
||||||
|
attributes: [
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
"avatar",
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
"status"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "user",
|
||||||
|
attributes: ["id", "username", "createdAt", "updatedAt"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
io.to(req.body.users[i]).emit("chatAdded", association)
|
||||||
|
const message = await Message.create({
|
||||||
|
userId: 0,
|
||||||
|
chatId: chat.chatId,
|
||||||
|
content: `${association.user.username} has been added by ${req.user.username}.`,
|
||||||
|
type: "join"
|
||||||
|
})
|
||||||
|
const messageLookup = await Message.findOne({
|
||||||
|
where: {
|
||||||
|
id: message.id
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Attachment,
|
||||||
|
as: "attachments"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Message,
|
||||||
|
as: "reply",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "user",
|
||||||
|
attributes: [
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
"avatar",
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Chat,
|
||||||
|
as: "chat",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "users",
|
||||||
|
attributes: [
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
"avatar",
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "user",
|
||||||
|
attributes: [
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
"avatar",
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
associations.forEach((association) => {
|
||||||
|
io.to(association.userId).emit("message", {
|
||||||
|
...messageLookup.dataValues,
|
||||||
|
associationId: association.id,
|
||||||
|
keyId: `${message.id}-${message.updatedAt.toISOString()}`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
res.sendStatus(204)
|
||||||
|
} else {
|
||||||
|
throw Errors.chatNotFoundOrNotAdmin
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.delete("/association/:id", 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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (chat) {
|
||||||
|
await chat.destroy()
|
||||||
|
res.sendStatus(204)
|
||||||
|
const message = await Message.create({
|
||||||
|
userId: 0,
|
||||||
|
chatId: chat.chatId,
|
||||||
|
content: `${req.user.username} has left the group.`,
|
||||||
|
type: "leave"
|
||||||
|
})
|
||||||
|
const associations = await ChatAssociation.findAll({
|
||||||
|
where: {
|
||||||
|
chatId: chat.chatId
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "user",
|
||||||
|
attributes: [
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
"avatar",
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
const messageLookup = await Message.findOne({
|
||||||
|
where: {
|
||||||
|
id: message.id
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Attachment,
|
||||||
|
as: "attachments"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Message,
|
||||||
|
as: "reply",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "user",
|
||||||
|
attributes: [
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
"avatar",
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: Chat,
|
||||||
|
as: "chat",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "users",
|
||||||
|
attributes: [
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
"avatar",
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "user",
|
||||||
|
attributes: [
|
||||||
|
"username",
|
||||||
|
"name",
|
||||||
|
"avatar",
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
associations.forEach((association) => {
|
||||||
|
io.to(association.userId).emit("message", {
|
||||||
|
...messageLookup.dataValues,
|
||||||
|
associationId: association.id,
|
||||||
|
keyId: `${message.id}-${message.updatedAt.toISOString()}`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
throw Errors.invalidParameter("chat association id")
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
next(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
|
@ -346,417 +346,6 @@ router.get("/mutual/:id/groups", auth, async (req, res, next) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
router.delete(
|
|
||||||
"/association/:id/:associationId",
|
|
||||||
auth,
|
|
||||||
async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const io = req.app.get("io")
|
|
||||||
const chat = await ChatAssociation.findOne({
|
|
||||||
where: {
|
|
||||||
id: req.params.id,
|
|
||||||
userId: req.user.id,
|
|
||||||
rank: "admin"
|
|
||||||
},
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Chat,
|
|
||||||
as: "chat",
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "users",
|
|
||||||
attributes: ["id", "username", "createdAt", "updatedAt"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
const association = await ChatAssociation.findOne({
|
|
||||||
where: {
|
|
||||||
id: req.params.associationId,
|
|
||||||
chatId: chat.chat.id
|
|
||||||
},
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Chat,
|
|
||||||
as: "chat"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "user",
|
|
||||||
attributes: ["id", "username", "createdAt", "updatedAt"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
if (!chat) {
|
|
||||||
throw Errors.chatNotFoundOrNotAdmin
|
|
||||||
}
|
|
||||||
if (!association) {
|
|
||||||
throw Errors.chatNotFoundOrNotAdmin
|
|
||||||
}
|
|
||||||
if(association.chat.type === "direct") {
|
|
||||||
throw Errors.invalidParameter("association id", "You cannot leave direct messages")
|
|
||||||
}
|
|
||||||
await association.destroy()
|
|
||||||
res.sendStatus(204)
|
|
||||||
const message = await Message.create({
|
|
||||||
userId: 0,
|
|
||||||
chatId: chat.chat.id,
|
|
||||||
content: `${association.user.username} has been removed by ${req.user.username}.`,
|
|
||||||
type: "leave"
|
|
||||||
})
|
|
||||||
const associations = await ChatAssociation.findAll({
|
|
||||||
where: {
|
|
||||||
chatId: chat.chat.id
|
|
||||||
},
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "user",
|
|
||||||
attributes: [
|
|
||||||
"username",
|
|
||||||
"name",
|
|
||||||
"avatar",
|
|
||||||
"id",
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
const messageLookup = await Message.findOne({
|
|
||||||
where: {
|
|
||||||
id: message.id
|
|
||||||
},
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Attachment,
|
|
||||||
as: "attachments"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: Message,
|
|
||||||
as: "reply",
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "user",
|
|
||||||
attributes: [
|
|
||||||
"username",
|
|
||||||
"name",
|
|
||||||
|
|
||||||
"avatar",
|
|
||||||
"id",
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: Chat,
|
|
||||||
as: "chat",
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "users",
|
|
||||||
attributes: [
|
|
||||||
"username",
|
|
||||||
"name",
|
|
||||||
|
|
||||||
"avatar",
|
|
||||||
"id",
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "user",
|
|
||||||
attributes: [
|
|
||||||
"username",
|
|
||||||
"name",
|
|
||||||
|
|
||||||
"avatar",
|
|
||||||
"id",
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
associations.forEach((association) => {
|
|
||||||
io.to(association.userId).emit("message", {
|
|
||||||
...messageLookup.dataValues,
|
|
||||||
associationId: association.id,
|
|
||||||
keyId: `${message.id}-${message.updatedAt.toISOString()}`
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
next(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
router.put("/association/:id/:associationId", auth, async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const chat = await ChatAssociation.findOne({
|
|
||||||
where: {
|
|
||||||
id: req.params.id,
|
|
||||||
userId: req.user.id,
|
|
||||||
rank: "admin"
|
|
||||||
},
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Chat,
|
|
||||||
as: "chat",
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "users",
|
|
||||||
attributes: ["id", "username", "createdAt", "updatedAt"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
const association = await ChatAssociation.findOne({
|
|
||||||
where: {
|
|
||||||
id: req.params.associationId,
|
|
||||||
chatId: chat.chat.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (!chat) {
|
|
||||||
throw Errors.chatNotFoundOrNotAdmin
|
|
||||||
}
|
|
||||||
if (!association) {
|
|
||||||
throw Errors.chatNotFoundOrNotAdmin
|
|
||||||
}
|
|
||||||
if (association.rank === "admin") {
|
|
||||||
throw Errors.chatNotFoundOrNotAdmin
|
|
||||||
}
|
|
||||||
await association.update({
|
|
||||||
rank: req.body.rank
|
|
||||||
})
|
|
||||||
res.sendStatus(204)
|
|
||||||
} catch (err) {
|
|
||||||
next(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
router.post("/association/:id", auth, async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const io = req.app.get("io")
|
|
||||||
const chat = await ChatAssociation.findOne({
|
|
||||||
where: {
|
|
||||||
userId: req.user.id,
|
|
||||||
chatId: req.params.id,
|
|
||||||
rank: "admin"
|
|
||||||
},
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Chat,
|
|
||||||
as: "chat",
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "users",
|
|
||||||
attributes: ["id", "username", "createdAt", "updatedAt"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
if (chat) {
|
|
||||||
if (req.body.users.length > 10) {
|
|
||||||
throw Errors.invalidParameter(
|
|
||||||
"User",
|
|
||||||
"The maximum number of users is 10"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!req.body.users.length) {
|
|
||||||
throw Errors.invalidParameter(
|
|
||||||
"User",
|
|
||||||
"You need at least 1 user to create a chat"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (req.body.users.includes(req.user.id)) {
|
|
||||||
throw Errors.invalidParameter(
|
|
||||||
"User",
|
|
||||||
"You cannot create a DM with yourself"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const friends = await Friend.findAll({
|
|
||||||
where: {
|
|
||||||
userId: req.user.id,
|
|
||||||
friendId: req.body.users,
|
|
||||||
status: "accepted"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (friends.length !== req.body.users.length) {
|
|
||||||
throw Errors.invalidParameter(
|
|
||||||
"User",
|
|
||||||
"You are not friends with this user"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const users = await ChatAssociation.findAll({
|
|
||||||
where: {
|
|
||||||
userId: req.body.users,
|
|
||||||
chatId: req.params.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (users.length > 0) {
|
|
||||||
throw Errors.invalidParameter(
|
|
||||||
"User",
|
|
||||||
"One or more users are already in this chat"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const associations = await ChatAssociation.findAll({
|
|
||||||
where: {
|
|
||||||
chatId: chat.chatId
|
|
||||||
},
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "user",
|
|
||||||
attributes: [
|
|
||||||
"username",
|
|
||||||
"name",
|
|
||||||
|
|
||||||
"avatar",
|
|
||||||
"id",
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
for (let i = 0; i < req.body.users.length; i++) {
|
|
||||||
const c1 = await ChatAssociation.create({
|
|
||||||
chatId: chat.chat.id,
|
|
||||||
userId: req.body.users[i],
|
|
||||||
rank: "member"
|
|
||||||
})
|
|
||||||
const association = await ChatAssociation.findOne({
|
|
||||||
where: {
|
|
||||||
id: c1.id
|
|
||||||
},
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Chat,
|
|
||||||
as: "chat",
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "users",
|
|
||||||
attributes: [
|
|
||||||
"username",
|
|
||||||
"name",
|
|
||||||
|
|
||||||
"avatar",
|
|
||||||
"id",
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt",
|
|
||||||
|
|
||||||
"status"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "user",
|
|
||||||
attributes: ["id", "username", "createdAt", "updatedAt"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
io.to(req.body.users[i]).emit("chatAdded", association)
|
|
||||||
const message = await Message.create({
|
|
||||||
userId: 0,
|
|
||||||
chatId: chat.chatId,
|
|
||||||
content: `${association.user.username} has been added by ${req.user.username}.`,
|
|
||||||
type: "join"
|
|
||||||
})
|
|
||||||
const messageLookup = await Message.findOne({
|
|
||||||
where: {
|
|
||||||
id: message.id
|
|
||||||
},
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Attachment,
|
|
||||||
as: "attachments"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: Message,
|
|
||||||
as: "reply",
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "user",
|
|
||||||
attributes: [
|
|
||||||
"username",
|
|
||||||
"name",
|
|
||||||
|
|
||||||
"avatar",
|
|
||||||
"id",
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: Chat,
|
|
||||||
as: "chat",
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "users",
|
|
||||||
attributes: [
|
|
||||||
"username",
|
|
||||||
"name",
|
|
||||||
|
|
||||||
"avatar",
|
|
||||||
"id",
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "user",
|
|
||||||
attributes: [
|
|
||||||
"username",
|
|
||||||
"name",
|
|
||||||
|
|
||||||
"avatar",
|
|
||||||
"id",
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
associations.forEach((association) => {
|
|
||||||
io.to(association.userId).emit("message", {
|
|
||||||
...messageLookup.dataValues,
|
|
||||||
associationId: association.id,
|
|
||||||
keyId: `${message.id}-${message.updatedAt.toISOString()}`
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
res.sendStatus(204)
|
|
||||||
} else {
|
|
||||||
throw Errors.chatNotFoundOrNotAdmin
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
next(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
router.get("/users", auth, async (req, res, next) => {
|
router.get("/users", auth, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const users = await User.findAll({
|
const users = await User.findAll({
|
||||||
|
@ -1127,117 +716,6 @@ router.get("/:id/search", auth, async (req, res, next) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
router.delete("/association/:id", 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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (chat) {
|
|
||||||
await chat.destroy()
|
|
||||||
res.sendStatus(204)
|
|
||||||
const message = await Message.create({
|
|
||||||
userId: 0,
|
|
||||||
chatId: chat.chatId,
|
|
||||||
content: `${req.user.username} has left the group.`,
|
|
||||||
type: "leave"
|
|
||||||
})
|
|
||||||
const associations = await ChatAssociation.findAll({
|
|
||||||
where: {
|
|
||||||
chatId: chat.chatId
|
|
||||||
},
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "user",
|
|
||||||
attributes: [
|
|
||||||
"username",
|
|
||||||
"name",
|
|
||||||
"avatar",
|
|
||||||
"id",
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
const messageLookup = await Message.findOne({
|
|
||||||
where: {
|
|
||||||
id: message.id
|
|
||||||
},
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Attachment,
|
|
||||||
as: "attachments"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: Message,
|
|
||||||
as: "reply",
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "user",
|
|
||||||
attributes: [
|
|
||||||
"username",
|
|
||||||
"name",
|
|
||||||
"avatar",
|
|
||||||
"id",
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: Chat,
|
|
||||||
as: "chat",
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "users",
|
|
||||||
attributes: [
|
|
||||||
"username",
|
|
||||||
"name",
|
|
||||||
"avatar",
|
|
||||||
"id",
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "user",
|
|
||||||
attributes: [
|
|
||||||
"username",
|
|
||||||
"name",
|
|
||||||
"avatar",
|
|
||||||
"id",
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
associations.forEach((association) => {
|
|
||||||
io.to(association.userId).emit("message", {
|
|
||||||
...messageLookup.dataValues,
|
|
||||||
associationId: association.id,
|
|
||||||
keyId: `${message.id}-${message.updatedAt.toISOString()}`
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
throw Errors.invalidParameter("chat association id")
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
next(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
router.delete("/:id/message/:mId", auth, async (req, res, next) => {
|
router.delete("/:id/message/:mId", auth, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const io = req.app.get("io")
|
const io = req.app.get("io")
|
||||||
|
@ -1944,7 +1422,7 @@ router.get("/:id/messages", auth, async (req, res, next) => {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
offset: req.query.offset || 0,
|
offset: parseInt(req.query.offset) || 0,
|
||||||
order: [["id", "DESC"]],
|
order: [["id", "DESC"]],
|
||||||
limit: 50
|
limit: 50
|
||||||
})
|
})
|
||||||
|
@ -2096,10 +1574,10 @@ router.post("/create", auth, async (req, res, next) => {
|
||||||
type
|
type
|
||||||
})
|
})
|
||||||
req.body.users.push(req.user.id)
|
req.body.users.push(req.user.id)
|
||||||
for (let i = 0; i < req.body.users.length; i++) {
|
for (const id of req.body.users) {
|
||||||
let rank
|
let rank
|
||||||
if (type === "group") {
|
if (type === "group") {
|
||||||
if (req.body.users[i] === req.user.id) {
|
if (id === req.user.id) {
|
||||||
rank = "admin"
|
rank = "admin"
|
||||||
} else {
|
} else {
|
||||||
rank = "member"
|
rank = "member"
|
||||||
|
@ -2107,14 +1585,17 @@ router.post("/create", auth, async (req, res, next) => {
|
||||||
} else {
|
} else {
|
||||||
rank = "member"
|
rank = "member"
|
||||||
}
|
}
|
||||||
const c1 = await ChatAssociation.create({
|
await ChatAssociation.create({
|
||||||
chatId: chat.id,
|
chatId: chat.id,
|
||||||
userId: req.body.users[i],
|
userId: id,
|
||||||
rank
|
rank
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
for (const id of req.body.users) {
|
||||||
const association = await ChatAssociation.findOne({
|
const association = await ChatAssociation.findOne({
|
||||||
where: {
|
where: {
|
||||||
id: c1.id
|
chatId: chat.id,
|
||||||
|
userId: id
|
||||||
},
|
},
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
|
@ -2178,7 +1659,7 @@ router.post("/create", auth, async (req, res, next) => {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
io.to(req.body.users[i]).emit("chatAdded", association)
|
io.to(id).emit("chatAdded", association)
|
||||||
}
|
}
|
||||||
const association = await ChatAssociation.findOne({
|
const association = await ChatAssociation.findOne({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "colubrina-chat",
|
"name": "colubrina-chat",
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": "Troplo <troplo@troplo.com>",
|
"author": "Troplo <troplo@troplo.com>",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
|
|
|
@ -742,7 +742,7 @@ export default {
|
||||||
removeUserFromGroup(user) {
|
removeUserFromGroup(user) {
|
||||||
this.axios
|
this.axios
|
||||||
.delete(
|
.delete(
|
||||||
"/api/v1/communications/association/" +
|
"/api/v1/association/" +
|
||||||
this.settings.item.id +
|
this.settings.item.id +
|
||||||
"/" +
|
"/" +
|
||||||
user.id
|
user.id
|
||||||
|
@ -757,7 +757,7 @@ export default {
|
||||||
giveUserAdmin(user) {
|
giveUserAdmin(user) {
|
||||||
this.axios
|
this.axios
|
||||||
.put(
|
.put(
|
||||||
"/api/v1/communications/association/" +
|
"/api/v1/association/" +
|
||||||
this.settings.item.id +
|
this.settings.item.id +
|
||||||
"/" +
|
"/" +
|
||||||
user.id,
|
user.id,
|
||||||
|
@ -798,7 +798,7 @@ export default {
|
||||||
addMembersToGroup() {
|
addMembersToGroup() {
|
||||||
this.axios
|
this.axios
|
||||||
.post(
|
.post(
|
||||||
"/api/v1/communications/association/" + this.settings.item.chat.id,
|
"/api/v1/association/" + this.settings.item.chat.id,
|
||||||
{
|
{
|
||||||
users: this.settings.addMembers.users
|
users: this.settings.addMembers.users
|
||||||
}
|
}
|
||||||
|
@ -817,7 +817,7 @@ export default {
|
||||||
},
|
},
|
||||||
leaveGroup() {
|
leaveGroup() {
|
||||||
this.axios
|
this.axios
|
||||||
.delete("/api/v1/communications/association/" + this.leave.item.id)
|
.delete("/api/v1/association/" + this.leave.item.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.leave.dialog = false
|
this.leave.dialog = false
|
||||||
this.$store.state.chats = this.$store.state.chats.filter(
|
this.$store.state.chats = this.$store.state.chats.filter(
|
||||||
|
|
693
frontend/src/components/Message.vue
Normal file
693
frontend/src/components/Message.vue
Normal file
|
@ -0,0 +1,693 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<template v-if="!message.type">
|
||||||
|
<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"
|
||||||
|
@contextmenu="show($event, 'message', message)"
|
||||||
|
>
|
||||||
|
<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-if="edit.id !== message.id"
|
||||||
|
v-markdown
|
||||||
|
style="overflow-wrap: anywhere"
|
||||||
|
>
|
||||||
|
{{ message.content }}
|
||||||
|
</p>
|
||||||
|
<template v-if="edit.id !== message.id">
|
||||||
|
<v-row
|
||||||
|
v-for="(embed, index) in message.embeds"
|
||||||
|
:key="index"
|
||||||
|
:id="'embed-' + index"
|
||||||
|
no-gutters
|
||||||
|
>
|
||||||
|
<v-card
|
||||||
|
elevaion="0"
|
||||||
|
:color="
|
||||||
|
embed.type === 'embed-v1' ? embed.backgroundColor : 'bg'
|
||||||
|
"
|
||||||
|
:max-width="400"
|
||||||
|
:min-width="!$vuetify.breakpoint.mobile ? 300 : 0"
|
||||||
|
class="ml-1 rounded-xl mb-1 mr-1"
|
||||||
|
>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-row v-if="embed.type === 'openGraph'">
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
class="text-xs-center"
|
||||||
|
v-if="embed.openGraph.ogImage"
|
||||||
|
>
|
||||||
|
<v-img
|
||||||
|
:src="
|
||||||
|
embed.openGraph.ogImage?.url ||
|
||||||
|
embed.openGraph.ogImage[0]?.url
|
||||||
|
"
|
||||||
|
class="elevation-1"
|
||||||
|
contain
|
||||||
|
:aspect-ratio="16 / 9"
|
||||||
|
>
|
||||||
|
<template v-slot:placeholder>
|
||||||
|
<v-row
|
||||||
|
class="fill-height ma-0"
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
>
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
color="grey lighten-5"
|
||||||
|
></v-progress-circular>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
</v-img>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" class="text-xs-center">
|
||||||
|
<h4>
|
||||||
|
{{ embed.openGraph.ogSiteName }}
|
||||||
|
</h4>
|
||||||
|
<a
|
||||||
|
:href="embed.link"
|
||||||
|
target="_blank"
|
||||||
|
style="text-decoration: none"
|
||||||
|
>
|
||||||
|
<h3>
|
||||||
|
{{ embed.openGraph.ogTitle }}
|
||||||
|
</h3>
|
||||||
|
</a>
|
||||||
|
<p v-if="embed.openGraph.ogDescription">
|
||||||
|
{{ embed.openGraph.ogDescription }}
|
||||||
|
</p>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<template v-else-if="embed.type === 'image'">
|
||||||
|
<v-hover v-slot="{ hover }">
|
||||||
|
<div>
|
||||||
|
<v-img
|
||||||
|
@click="setImagePreview(embed)"
|
||||||
|
contain
|
||||||
|
:aspect-ratio="16 / 9"
|
||||||
|
:src="embed.mediaProxyLink"
|
||||||
|
>
|
||||||
|
<template v-slot:placeholder>
|
||||||
|
<v-row
|
||||||
|
class="fill-height ma-0"
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
>
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
color="grey lighten-5"
|
||||||
|
></v-progress-circular>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
<template v-slot:default>
|
||||||
|
<v-fade-transition v-if="hover">
|
||||||
|
<v-overlay absolute>
|
||||||
|
<v-icon large>mdi-arrow-expand-all</v-icon>
|
||||||
|
</v-overlay>
|
||||||
|
</v-fade-transition>
|
||||||
|
</template>
|
||||||
|
</v-img>
|
||||||
|
</div>
|
||||||
|
</v-hover>
|
||||||
|
<v-card-actions>
|
||||||
|
MediaProxy Image
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn text icon :href="embed.url" target="_blank">
|
||||||
|
<v-icon> mdi-download </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</template>
|
||||||
|
<v-row v-else-if="embed.type === 'embed-v1'">
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
class="text-xs-center"
|
||||||
|
v-if="embed.headerImage"
|
||||||
|
>
|
||||||
|
<v-img
|
||||||
|
:src="
|
||||||
|
embed.openGraph.headerImage?.url ||
|
||||||
|
embed.openGraph.headerImage[0]?.url
|
||||||
|
"
|
||||||
|
class="elevation-1"
|
||||||
|
contain
|
||||||
|
:aspect-ratio="16 / 9"
|
||||||
|
>
|
||||||
|
<template v-slot:placeholder>
|
||||||
|
<v-row
|
||||||
|
class="fill-height ma-0"
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
>
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
color="grey lighten-5"
|
||||||
|
></v-progress-circular>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
</v-img>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" class="text-xs-center">
|
||||||
|
<h4 v-if="embed.title">
|
||||||
|
{{ embed.title }}
|
||||||
|
</h4>
|
||||||
|
<p v-if="embed.description">
|
||||||
|
{{ embed.description }}
|
||||||
|
</p>
|
||||||
|
<v-row
|
||||||
|
v-for="(graph, index) in embed.graphs"
|
||||||
|
:key="'graph-' + index"
|
||||||
|
>
|
||||||
|
<v-col cols="12" class="text-xs-center">
|
||||||
|
<h3>
|
||||||
|
{{ graph.name }}
|
||||||
|
</h3>
|
||||||
|
<p>Chart data could not be loaded.</p>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row
|
||||||
|
v-for="(field, index) in embed.fields"
|
||||||
|
:key="'field-' + index"
|
||||||
|
:id="'field-' + index"
|
||||||
|
class="mt-1"
|
||||||
|
>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
class="text-xs-center"
|
||||||
|
style="white-space: pre-wrap"
|
||||||
|
>
|
||||||
|
<h4>{{ field.name }}</h4>
|
||||||
|
<p>{{ field.value }}</p>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<a
|
||||||
|
:href="embed.link.url"
|
||||||
|
v-if="embed.link"
|
||||||
|
target="_blank"
|
||||||
|
style="text-decoration: none"
|
||||||
|
>
|
||||||
|
<h3>
|
||||||
|
{{ embed.link.title }}
|
||||||
|
</h3>
|
||||||
|
</a>
|
||||||
|
<small v-if="embed.footer">
|
||||||
|
{{ embed.footer }}
|
||||||
|
</small>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-else>
|
||||||
|
<v-container>
|
||||||
|
<h4>You must update Colubrina to see this embed.</h4>
|
||||||
|
</v-container>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-card>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
<template v-if="edit.id !== message.id">
|
||||||
|
<v-card
|
||||||
|
v-for="(attachment, index) in message.attachments"
|
||||||
|
:key="attachment.id"
|
||||||
|
:id="'attachment-' + index"
|
||||||
|
max-width="40%"
|
||||||
|
elevaion="0"
|
||||||
|
color="card"
|
||||||
|
>
|
||||||
|
<v-hover
|
||||||
|
v-slot="{ hover }"
|
||||||
|
v-if="
|
||||||
|
attachment.extension === 'jpg' ||
|
||||||
|
attachment.extension === 'png' ||
|
||||||
|
attachment.extension === 'jpeg' ||
|
||||||
|
attachment.extension === 'gif'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<v-img
|
||||||
|
@click="setImagePreview(attachment)"
|
||||||
|
contain
|
||||||
|
:aspect-ratio="16 / 9"
|
||||||
|
:src="
|
||||||
|
$store.state.baseURL +
|
||||||
|
'/usercontent/' +
|
||||||
|
attachment.attachment
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template v-slot:placeholder>
|
||||||
|
<v-row
|
||||||
|
class="fill-height ma-0"
|
||||||
|
align="center"
|
||||||
|
justify="center"
|
||||||
|
>
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
color="grey lighten-5"
|
||||||
|
></v-progress-circular>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
<template v-slot:default>
|
||||||
|
<v-fade-transition v-if="hover">
|
||||||
|
<v-overlay absolute>
|
||||||
|
<v-icon large>mdi-arrow-expand-all</v-icon>
|
||||||
|
</v-overlay>
|
||||||
|
</v-fade-transition>
|
||||||
|
</template>
|
||||||
|
</v-img>
|
||||||
|
</div>
|
||||||
|
</v-hover>
|
||||||
|
<v-card-text v-else>
|
||||||
|
<v-icon class="mr-2" :size="48">
|
||||||
|
{{ fileTypes[attachment.extension] || "mdi-file" }}
|
||||||
|
</v-icon>
|
||||||
|
<span>
|
||||||
|
{{ attachment.name }}
|
||||||
|
</span>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
{{ attachment.name }} -
|
||||||
|
{{ friendlySize(attachment.size) }}
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
text
|
||||||
|
icon
|
||||||
|
:href="
|
||||||
|
$store.state.baseURL +
|
||||||
|
'/usercontent/' +
|
||||||
|
attachment.attachment
|
||||||
|
"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-download </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
<CommsInput
|
||||||
|
:edit="edit"
|
||||||
|
:chat="chat"
|
||||||
|
:auto-scroll="autoScroll"
|
||||||
|
:end-edit="endEdit"
|
||||||
|
v-if="edit.id === message.id"
|
||||||
|
></CommsInput>
|
||||||
|
</v-list-item-content>
|
||||||
|
<v-list-item-action v-if="!$vuetify.breakpoint.mobile">
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
v-if="message.userId === $store.state.user.id"
|
||||||
|
@click="deleteMessage(message)"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-delete </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
edit.content = message.content
|
||||||
|
edit.editing = true
|
||||||
|
edit.id = message.id
|
||||||
|
"
|
||||||
|
v-if="
|
||||||
|
message.userId === $store.state.user.id &&
|
||||||
|
edit.id !== message.id
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-pencil </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
edit.content = ''
|
||||||
|
edit.editing = false
|
||||||
|
edit.id = null
|
||||||
|
"
|
||||||
|
v-if="
|
||||||
|
message.userId === $store.state.user.id &&
|
||||||
|
edit.id === message.id
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-close </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
replying(message)
|
||||||
|
focusInput()
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-reply </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item-action>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="message.type === 'leave'">
|
||||||
|
<v-list-item :key="message.keyId" :id="'message-' + index">
|
||||||
|
<v-icon color="red" class="mr-2 ml-1"> mdi-arrow-left </v-icon>
|
||||||
|
<v-list-item-content>
|
||||||
|
{{ message.content }}
|
||||||
|
</v-list-item-content>
|
||||||
|
<v-list-item-action>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
{{ $date(message.createdAt).format("DD/MM/YYYY hh:mm A") }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
v-if="message.userId === $store.state.user.id"
|
||||||
|
@click="deleteMessage(message)"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-delete </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
edit.content = message.content
|
||||||
|
edit.editing = true
|
||||||
|
edit.id = message.id
|
||||||
|
"
|
||||||
|
v-if="
|
||||||
|
message.userId === $store.state.user.id &&
|
||||||
|
edit.id !== message.id
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-pencil </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
edit.content = ''
|
||||||
|
edit.editing = false
|
||||||
|
edit.id = null
|
||||||
|
"
|
||||||
|
v-if="
|
||||||
|
message.userId === $store.state.user.id &&
|
||||||
|
edit.id === message.id
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-close </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
replying(message)
|
||||||
|
focusInput()
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-reply </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item-action>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="message.type === 'join'">
|
||||||
|
<v-list-item :key="message.keyId" :id="'message-' + index">
|
||||||
|
<v-icon color="success" class="mr-2 ml-1"> mdi-arrow-right </v-icon>
|
||||||
|
<v-list-item-content>
|
||||||
|
{{ message.content }}
|
||||||
|
</v-list-item-content>
|
||||||
|
<v-list-item-action>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
{{ $date(message.createdAt).format("DD/MM/YYYY hh:mm A") }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
v-if="message.userId === $store.state.user.id"
|
||||||
|
@click="deleteMessage(message)"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-delete </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
edit.content = message.content
|
||||||
|
edit.editing = true
|
||||||
|
edit.id = message.id
|
||||||
|
"
|
||||||
|
v-if="
|
||||||
|
message.userId === $store.state.user.id &&
|
||||||
|
edit.id !== message.id
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-pencil </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
edit.content = ''
|
||||||
|
edit.editing = false
|
||||||
|
edit.id = null
|
||||||
|
"
|
||||||
|
v-if="
|
||||||
|
message.userId === $store.state.user.id &&
|
||||||
|
edit.id === message.id
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-close </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
replying(message)
|
||||||
|
focusInput()
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-reply </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item-action>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="message.type === 'rename'">
|
||||||
|
<v-list-item :key="message.keyId" :id="'message-' + index">
|
||||||
|
<v-icon color="grey" class="mr-2 ml-1"> mdi-pencil </v-icon>
|
||||||
|
<v-list-item-content>
|
||||||
|
{{ message.content }}
|
||||||
|
</v-list-item-content>
|
||||||
|
<v-list-item-action>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
{{ $date(message.createdAt).format("DD/MM/YYYY hh:mm A") }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
v-if="message.userId === $store.state.user.id"
|
||||||
|
@click="deleteMessage(message)"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-delete </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
edit.content = message.content
|
||||||
|
edit.editing = true
|
||||||
|
edit.id = message.id
|
||||||
|
"
|
||||||
|
v-if="
|
||||||
|
message.userId === $store.state.user.id &&
|
||||||
|
edit.id !== message.id
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-pencil </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
edit.content = ''
|
||||||
|
edit.editing = false
|
||||||
|
edit.id = null
|
||||||
|
"
|
||||||
|
v-if="
|
||||||
|
message.userId === $store.state.user.id &&
|
||||||
|
edit.id === message.id
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-close </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
replying(message)
|
||||||
|
focusInput()
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-reply </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item-action>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="message.type === 'system'">
|
||||||
|
<v-list-item :key="message.keyId" :id="'message-' + index">
|
||||||
|
<v-icon color="grey" class="mr-2 ml-1"> mdi-pencil </v-icon>
|
||||||
|
<v-list-item-content>
|
||||||
|
{{ message.content }}
|
||||||
|
</v-list-item-content>
|
||||||
|
<v-list-item-action>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
{{ $date(message.createdAt).format("DD/MM/YYYY hh:mm A") }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
v-if="message.userId === $store.state.user.id"
|
||||||
|
@click="deleteMessage(message)"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-delete </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
edit.content = message.content
|
||||||
|
edit.editing = true
|
||||||
|
edit.id = message.id
|
||||||
|
"
|
||||||
|
v-if="
|
||||||
|
message.userId === $store.state.user.id &&
|
||||||
|
edit.id !== message.id
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-pencil </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
edit.content = ''
|
||||||
|
edit.editing = false
|
||||||
|
edit.id = null
|
||||||
|
"
|
||||||
|
v-if="
|
||||||
|
message.userId === $store.state.user.id &&
|
||||||
|
edit.id === message.id
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-close </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="
|
||||||
|
replying(message)
|
||||||
|
focusInput()
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-icon> mdi-reply </v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</v-list-item-action>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CommsInput from "./CommsInput.vue"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Message",
|
||||||
|
props: [
|
||||||
|
"message",
|
||||||
|
"edit",
|
||||||
|
"jumpToMessage",
|
||||||
|
"focusInput",
|
||||||
|
"replying",
|
||||||
|
"getName",
|
||||||
|
"chat",
|
||||||
|
"endEdit",
|
||||||
|
"autoScroll",
|
||||||
|
"index",
|
||||||
|
"show"
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
CommsInput
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
|
@ -18,7 +18,7 @@ const md = require("markdown-it")({
|
||||||
html: false, // Enable HTML tags in source
|
html: false, // Enable HTML tags in source
|
||||||
xhtmlOut: false, // Use '/' to close single tags (<br />).
|
xhtmlOut: false, // Use '/' to close single tags (<br />).
|
||||||
// This is only for full CommonMark compatibility.
|
// This is only for full CommonMark compatibility.
|
||||||
breaks: false, // Convert '\n' in paragraphs into <br>
|
breaks: true, // Convert '\n' in paragraphs into <br>
|
||||||
langPrefix: "language-", // CSS language prefix for fenced blocks. Can be
|
langPrefix: "language-", // CSS language prefix for fenced blocks. Can be
|
||||||
// useful for external highlighters.
|
// useful for external highlighters.
|
||||||
linkify: true, // Autoconvert URL-like text to links
|
linkify: true, // Autoconvert URL-like text to links
|
||||||
|
|
|
@ -22,7 +22,7 @@ export default {
|
||||||
selectedChat() {
|
selectedChat() {
|
||||||
try {
|
try {
|
||||||
return this.$store.state.chats.find(
|
return this.$store.state.chats.find(
|
||||||
(item) => item.id === JSON.parse(this.$route.params.id)
|
(item) => item.id === parseInt(this.$route.params.id)
|
||||||
)
|
)
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -125,710 +125,37 @@
|
||||||
color="card"
|
color="card"
|
||||||
elevation="0"
|
elevation="0"
|
||||||
>
|
>
|
||||||
<v-card-text class="flex-grow-1 overflow-y-auto">
|
<v-card-text class="flex-grow-1 overflow-y-auto" id="message-list">
|
||||||
<v-list two-line color="card" ref="message-list">
|
<v-list two-line color="card" ref="message-list">
|
||||||
|
<v-card-title v-if="reachedTop">
|
||||||
|
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-progress-circular
|
||||||
|
v-if="loadingMessages"
|
||||||
|
indeterminate
|
||||||
|
size="128"
|
||||||
|
class="justify-center"
|
||||||
|
></v-progress-circular>
|
||||||
<template v-for="(message, index) in messages">
|
<template v-for="(message, index) in messages">
|
||||||
<template v-if="!message.type">
|
<Message
|
||||||
<v-toolbar
|
:key="message.keyId"
|
||||||
@click="jumpToMessage(message.replyId)"
|
:message="message"
|
||||||
:key="message.keyId + '-reply-toolbar'"
|
:jump-to-message="jumpToMessage"
|
||||||
elevation="0"
|
:edit="edit"
|
||||||
height="40"
|
:focus-input="focusInput"
|
||||||
color="card"
|
:replying="setReply"
|
||||||
v-if="message.reply"
|
:get-name="getName"
|
||||||
style="cursor: pointer"
|
:end-edit="endEdit"
|
||||||
>
|
:auto-scroll="autoScroll"
|
||||||
<v-icon class="mr-2">mdi-reply</v-icon>
|
:chat="chat"
|
||||||
<v-avatar size="24" class="mr-2">
|
:index="index"
|
||||||
<v-img
|
:show="show"
|
||||||
:src="
|
></Message>
|
||||||
$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"
|
|
||||||
@contextmenu="show($event, 'message', message)"
|
|
||||||
>
|
|
||||||
<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-if="edit.id !== message.id"
|
|
||||||
v-markdown
|
|
||||||
style="overflow-wrap: anywhere"
|
|
||||||
>
|
|
||||||
{{ message.content }}
|
|
||||||
</p>
|
|
||||||
<template v-if="edit.id !== message.id">
|
|
||||||
<v-row
|
|
||||||
v-for="(embed, index) in message.embeds"
|
|
||||||
:key="index"
|
|
||||||
:id="'embed-' + index"
|
|
||||||
no-gutters
|
|
||||||
>
|
|
||||||
<v-card
|
|
||||||
elevaion="0"
|
|
||||||
:color="
|
|
||||||
embed.type === 'embed-v1'
|
|
||||||
? embed.backgroundColor
|
|
||||||
: 'bg'
|
|
||||||
"
|
|
||||||
:max-width="400"
|
|
||||||
:min-width="!$vuetify.breakpoint.mobile ? 300 : 0"
|
|
||||||
class="ml-1 rounded-xl mb-1 mr-1"
|
|
||||||
>
|
|
||||||
<v-container fluid>
|
|
||||||
<v-row v-if="embed.type === 'openGraph'">
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
class="text-xs-center"
|
|
||||||
v-if="embed.openGraph.ogImage"
|
|
||||||
>
|
|
||||||
<v-img
|
|
||||||
:src="
|
|
||||||
embed.openGraph.ogImage?.url ||
|
|
||||||
embed.openGraph.ogImage[0]?.url
|
|
||||||
"
|
|
||||||
class="elevation-1"
|
|
||||||
contain
|
|
||||||
:aspect-ratio="16 / 9"
|
|
||||||
>
|
|
||||||
<template v-slot:placeholder>
|
|
||||||
<v-row
|
|
||||||
class="fill-height ma-0"
|
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
>
|
|
||||||
<v-progress-circular
|
|
||||||
indeterminate
|
|
||||||
color="grey lighten-5"
|
|
||||||
></v-progress-circular>
|
|
||||||
</v-row>
|
|
||||||
</template>
|
|
||||||
</v-img>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" class="text-xs-center">
|
|
||||||
<h4>
|
|
||||||
{{ embed.openGraph.ogSiteName }}
|
|
||||||
</h4>
|
|
||||||
<a
|
|
||||||
:href="embed.link"
|
|
||||||
target="_blank"
|
|
||||||
style="text-decoration: none"
|
|
||||||
>
|
|
||||||
<h3>
|
|
||||||
{{ embed.openGraph.ogTitle }}
|
|
||||||
</h3>
|
|
||||||
</a>
|
|
||||||
<p v-if="embed.openGraph.ogDescription">
|
|
||||||
{{ embed.openGraph.ogDescription }}
|
|
||||||
</p>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
<template v-else-if="embed.type === 'image'">
|
|
||||||
<v-hover v-slot="{ hover }">
|
|
||||||
<div>
|
|
||||||
<v-img
|
|
||||||
@click="setImagePreview(embed)"
|
|
||||||
contain
|
|
||||||
:aspect-ratio="16 / 9"
|
|
||||||
:src="embed.mediaProxyLink"
|
|
||||||
>
|
|
||||||
<template v-slot:placeholder>
|
|
||||||
<v-row
|
|
||||||
class="fill-height ma-0"
|
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
>
|
|
||||||
<v-progress-circular
|
|
||||||
indeterminate
|
|
||||||
color="grey lighten-5"
|
|
||||||
></v-progress-circular>
|
|
||||||
</v-row>
|
|
||||||
</template>
|
|
||||||
<template v-slot:default>
|
|
||||||
<v-fade-transition v-if="hover">
|
|
||||||
<v-overlay absolute>
|
|
||||||
<v-icon large
|
|
||||||
>mdi-arrow-expand-all</v-icon
|
|
||||||
>
|
|
||||||
</v-overlay>
|
|
||||||
</v-fade-transition>
|
|
||||||
</template>
|
|
||||||
</v-img>
|
|
||||||
</div>
|
|
||||||
</v-hover>
|
|
||||||
<v-card-actions>
|
|
||||||
MediaProxy Image
|
|
||||||
<v-spacer />
|
|
||||||
<v-btn
|
|
||||||
text
|
|
||||||
icon
|
|
||||||
:href="embed.url"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-download </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</template>
|
|
||||||
<v-row v-else-if="embed.type === 'embed-v1'">
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
class="text-xs-center"
|
|
||||||
v-if="embed.headerImage"
|
|
||||||
>
|
|
||||||
<v-img
|
|
||||||
:src="
|
|
||||||
embed.openGraph.headerImage?.url ||
|
|
||||||
embed.openGraph.headerImage[0]?.url
|
|
||||||
"
|
|
||||||
class="elevation-1"
|
|
||||||
contain
|
|
||||||
:aspect-ratio="16 / 9"
|
|
||||||
>
|
|
||||||
<template v-slot:placeholder>
|
|
||||||
<v-row
|
|
||||||
class="fill-height ma-0"
|
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
>
|
|
||||||
<v-progress-circular
|
|
||||||
indeterminate
|
|
||||||
color="grey lighten-5"
|
|
||||||
></v-progress-circular>
|
|
||||||
</v-row>
|
|
||||||
</template>
|
|
||||||
</v-img>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" class="text-xs-center">
|
|
||||||
<h4 v-if="embed.title">
|
|
||||||
{{ embed.title }}
|
|
||||||
</h4>
|
|
||||||
<p v-if="embed.description">
|
|
||||||
{{ embed.description }}
|
|
||||||
</p>
|
|
||||||
<v-row
|
|
||||||
v-for="(graph, index) in embed.graphs"
|
|
||||||
:key="'graph-' + index"
|
|
||||||
>
|
|
||||||
<v-col cols="12" class="text-xs-center">
|
|
||||||
<h3>
|
|
||||||
{{ graph.name }}
|
|
||||||
</h3>
|
|
||||||
<Chart
|
|
||||||
:chart-data="graph.data"
|
|
||||||
v-if="graph.data"
|
|
||||||
:options="graphOptions"
|
|
||||||
></Chart>
|
|
||||||
<p v-else>
|
|
||||||
Chart data could not be loaded.
|
|
||||||
</p>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
<v-row
|
|
||||||
v-for="(field, index) in embed.fields"
|
|
||||||
:key="'field-' + index"
|
|
||||||
:id="'field-' + index"
|
|
||||||
class="mt-1"
|
|
||||||
>
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
class="text-xs-center"
|
|
||||||
style="white-space: pre-wrap"
|
|
||||||
>
|
|
||||||
<h4>{{ field.name }}</h4>
|
|
||||||
<p>{{ field.value }}</p>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
<a
|
|
||||||
:href="embed.link.url"
|
|
||||||
v-if="embed.link"
|
|
||||||
target="_blank"
|
|
||||||
style="text-decoration: none"
|
|
||||||
>
|
|
||||||
<h3>
|
|
||||||
{{ embed.link.title }}
|
|
||||||
</h3>
|
|
||||||
</a>
|
|
||||||
<small v-if="embed.footer">
|
|
||||||
{{ embed.footer }}
|
|
||||||
</small>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
<v-row v-else>
|
|
||||||
<v-container>
|
|
||||||
<h4>
|
|
||||||
You must update Colubrina to see this embed.
|
|
||||||
</h4>
|
|
||||||
</v-container>
|
|
||||||
</v-row>
|
|
||||||
</v-container>
|
|
||||||
</v-card>
|
|
||||||
</v-row>
|
|
||||||
</template>
|
|
||||||
<template v-if="edit.id !== message.id">
|
|
||||||
<v-card
|
|
||||||
v-for="(attachment, index) in message.attachments"
|
|
||||||
:key="attachment.id"
|
|
||||||
:id="'attachment-' + index"
|
|
||||||
max-width="40%"
|
|
||||||
elevaion="0"
|
|
||||||
color="card"
|
|
||||||
>
|
|
||||||
<v-hover
|
|
||||||
v-slot="{ hover }"
|
|
||||||
v-if="
|
|
||||||
attachment.extension === 'jpg' ||
|
|
||||||
attachment.extension === 'png' ||
|
|
||||||
attachment.extension === 'jpeg' ||
|
|
||||||
attachment.extension === 'gif'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<v-img
|
|
||||||
@click="setImagePreview(attachment)"
|
|
||||||
contain
|
|
||||||
:aspect-ratio="16 / 9"
|
|
||||||
:src="
|
|
||||||
$store.state.baseURL +
|
|
||||||
'/usercontent/' +
|
|
||||||
attachment.attachment
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<template v-slot:placeholder>
|
|
||||||
<v-row
|
|
||||||
class="fill-height ma-0"
|
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
>
|
|
||||||
<v-progress-circular
|
|
||||||
indeterminate
|
|
||||||
color="grey lighten-5"
|
|
||||||
></v-progress-circular>
|
|
||||||
</v-row>
|
|
||||||
</template>
|
|
||||||
<template v-slot:default>
|
|
||||||
<v-fade-transition v-if="hover">
|
|
||||||
<v-overlay absolute>
|
|
||||||
<v-icon large
|
|
||||||
>mdi-arrow-expand-all</v-icon
|
|
||||||
>
|
|
||||||
</v-overlay>
|
|
||||||
</v-fade-transition>
|
|
||||||
</template>
|
|
||||||
</v-img>
|
|
||||||
</div>
|
|
||||||
</v-hover>
|
|
||||||
<v-card-text v-else>
|
|
||||||
<v-icon class="mr-2" :size="48">
|
|
||||||
{{
|
|
||||||
fileTypes[attachment.extension] || "mdi-file"
|
|
||||||
}}
|
|
||||||
</v-icon>
|
|
||||||
<span>
|
|
||||||
{{ attachment.name }}
|
|
||||||
</span>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions>
|
|
||||||
{{ attachment.name }} -
|
|
||||||
{{ friendlySize(attachment.size) }}
|
|
||||||
<v-spacer />
|
|
||||||
<v-btn
|
|
||||||
text
|
|
||||||
icon
|
|
||||||
:href="
|
|
||||||
$store.state.baseURL +
|
|
||||||
'/usercontent/' +
|
|
||||||
attachment.attachment
|
|
||||||
"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-download </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</template>
|
|
||||||
<CommsInput
|
|
||||||
:edit="edit"
|
|
||||||
:chat="chat"
|
|
||||||
:auto-scroll="autoScroll"
|
|
||||||
:end-edit="endEdit"
|
|
||||||
v-if="edit.id === message.id"
|
|
||||||
></CommsInput>
|
|
||||||
</v-list-item-content>
|
|
||||||
<v-list-item-action v-if="!$vuetify.breakpoint.mobile">
|
|
||||||
<v-list-item-subtitle>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
v-if="message.userId === $store.state.user.id"
|
|
||||||
@click="deleteMessage(message)"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-delete </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click="
|
|
||||||
edit.content = message.content
|
|
||||||
edit.editing = true
|
|
||||||
edit.id = message.id
|
|
||||||
"
|
|
||||||
v-if="
|
|
||||||
message.userId === $store.state.user.id &&
|
|
||||||
edit.id !== message.id
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-pencil </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click="
|
|
||||||
edit.content = ''
|
|
||||||
edit.editing = false
|
|
||||||
edit.id = null
|
|
||||||
"
|
|
||||||
v-if="
|
|
||||||
message.userId === $store.state.user.id &&
|
|
||||||
edit.id === message.id
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-close </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click="
|
|
||||||
replying = message
|
|
||||||
focusInput()
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-reply </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-list-item-subtitle>
|
|
||||||
</v-list-item-action>
|
|
||||||
</v-list-item>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="message.type === 'leave'">
|
|
||||||
<v-list-item :key="message.keyId" :id="'message-' + index">
|
|
||||||
<v-icon color="red" class="mr-2 ml-1">
|
|
||||||
mdi-arrow-left
|
|
||||||
</v-icon>
|
|
||||||
<v-list-item-content>
|
|
||||||
{{ message.content }}
|
|
||||||
</v-list-item-content>
|
|
||||||
<v-list-item-action>
|
|
||||||
<v-list-item-subtitle>
|
|
||||||
{{
|
|
||||||
$date(message.createdAt).format("DD/MM/YYYY hh:mm A")
|
|
||||||
}}
|
|
||||||
</v-list-item-subtitle>
|
|
||||||
<v-list-item-subtitle>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
v-if="message.userId === $store.state.user.id"
|
|
||||||
@click="deleteMessage(message)"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-delete </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click="
|
|
||||||
edit.content = message.content
|
|
||||||
edit.editing = true
|
|
||||||
edit.id = message.id
|
|
||||||
"
|
|
||||||
v-if="
|
|
||||||
message.userId === $store.state.user.id &&
|
|
||||||
edit.id !== message.id
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-pencil </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click="
|
|
||||||
edit.content = ''
|
|
||||||
edit.editing = false
|
|
||||||
edit.id = null
|
|
||||||
"
|
|
||||||
v-if="
|
|
||||||
message.userId === $store.state.user.id &&
|
|
||||||
edit.id === message.id
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-close </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click="
|
|
||||||
replying = message
|
|
||||||
focusInput()
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-reply </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-list-item-subtitle>
|
|
||||||
</v-list-item-action>
|
|
||||||
</v-list-item>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="message.type === 'join'">
|
|
||||||
<v-list-item :key="message.keyId" :id="'message-' + index">
|
|
||||||
<v-icon color="success" class="mr-2 ml-1">
|
|
||||||
mdi-arrow-right
|
|
||||||
</v-icon>
|
|
||||||
<v-list-item-content>
|
|
||||||
{{ message.content }}
|
|
||||||
</v-list-item-content>
|
|
||||||
<v-list-item-action>
|
|
||||||
<v-list-item-subtitle>
|
|
||||||
{{
|
|
||||||
$date(message.createdAt).format("DD/MM/YYYY hh:mm A")
|
|
||||||
}}
|
|
||||||
</v-list-item-subtitle>
|
|
||||||
<v-list-item-subtitle>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
v-if="message.userId === $store.state.user.id"
|
|
||||||
@click="deleteMessage(message)"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-delete </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click="
|
|
||||||
edit.content = message.content
|
|
||||||
edit.editing = true
|
|
||||||
edit.id = message.id
|
|
||||||
"
|
|
||||||
v-if="
|
|
||||||
message.userId === $store.state.user.id &&
|
|
||||||
edit.id !== message.id
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-pencil </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click="
|
|
||||||
edit.content = ''
|
|
||||||
edit.editing = false
|
|
||||||
edit.id = null
|
|
||||||
"
|
|
||||||
v-if="
|
|
||||||
message.userId === $store.state.user.id &&
|
|
||||||
edit.id === message.id
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-close </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click="
|
|
||||||
replying = message
|
|
||||||
focusInput()
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-reply </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-list-item-subtitle>
|
|
||||||
</v-list-item-action>
|
|
||||||
</v-list-item>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="message.type === 'rename'">
|
|
||||||
<v-list-item :key="message.keyId" :id="'message-' + index">
|
|
||||||
<v-icon color="grey" class="mr-2 ml-1"> mdi-pencil </v-icon>
|
|
||||||
<v-list-item-content>
|
|
||||||
{{ message.content }}
|
|
||||||
</v-list-item-content>
|
|
||||||
<v-list-item-action>
|
|
||||||
<v-list-item-subtitle>
|
|
||||||
{{
|
|
||||||
$date(message.createdAt).format("DD/MM/YYYY hh:mm A")
|
|
||||||
}}
|
|
||||||
</v-list-item-subtitle>
|
|
||||||
<v-list-item-subtitle>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
v-if="message.userId === $store.state.user.id"
|
|
||||||
@click="deleteMessage(message)"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-delete </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click="
|
|
||||||
edit.content = message.content
|
|
||||||
edit.editing = true
|
|
||||||
edit.id = message.id
|
|
||||||
"
|
|
||||||
v-if="
|
|
||||||
message.userId === $store.state.user.id &&
|
|
||||||
edit.id !== message.id
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-pencil </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click="
|
|
||||||
edit.content = ''
|
|
||||||
edit.editing = false
|
|
||||||
edit.id = null
|
|
||||||
"
|
|
||||||
v-if="
|
|
||||||
message.userId === $store.state.user.id &&
|
|
||||||
edit.id === message.id
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-close </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click="
|
|
||||||
replying = message
|
|
||||||
focusInput()
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-reply </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-list-item-subtitle>
|
|
||||||
</v-list-item-action>
|
|
||||||
</v-list-item>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="message.type === 'system'">
|
|
||||||
<v-list-item :key="message.keyId" :id="'message-' + index">
|
|
||||||
<v-icon color="grey" class="mr-2 ml-1"> mdi-pencil </v-icon>
|
|
||||||
<v-list-item-content>
|
|
||||||
{{ message.content }}
|
|
||||||
</v-list-item-content>
|
|
||||||
<v-list-item-action>
|
|
||||||
<v-list-item-subtitle>
|
|
||||||
{{
|
|
||||||
$date(message.createdAt).format("DD/MM/YYYY hh:mm A")
|
|
||||||
}}
|
|
||||||
</v-list-item-subtitle>
|
|
||||||
<v-list-item-subtitle>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
v-if="message.userId === $store.state.user.id"
|
|
||||||
@click="deleteMessage(message)"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-delete </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click="
|
|
||||||
edit.content = message.content
|
|
||||||
edit.editing = true
|
|
||||||
edit.id = message.id
|
|
||||||
"
|
|
||||||
v-if="
|
|
||||||
message.userId === $store.state.user.id &&
|
|
||||||
edit.id !== message.id
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-pencil </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click="
|
|
||||||
edit.content = ''
|
|
||||||
edit.editing = false
|
|
||||||
edit.id = null
|
|
||||||
"
|
|
||||||
v-if="
|
|
||||||
message.userId === $store.state.user.id &&
|
|
||||||
edit.id === message.id
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-close </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click="
|
|
||||||
replying = message
|
|
||||||
focusInput()
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<v-icon> mdi-reply </v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-list-item-subtitle>
|
|
||||||
</v-list-item-action>
|
|
||||||
</v-list-item>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
@ -1353,34 +680,16 @@
|
||||||
<script>
|
<script>
|
||||||
import AjaxErrorHandler from "@/lib/errorHandler"
|
import AjaxErrorHandler from "@/lib/errorHandler"
|
||||||
import CommsInput from "@/components/CommsInput"
|
import CommsInput from "@/components/CommsInput"
|
||||||
import { Line as Chart } from "vue-chartjs/legacy"
|
|
||||||
import {
|
|
||||||
Chart as ChartJS,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement
|
|
||||||
} from "chart.js"
|
|
||||||
import NicknameDialog from "@/components/NicknameDialog"
|
import NicknameDialog from "@/components/NicknameDialog"
|
||||||
import UserDialog from "@/components/UserDialog"
|
import UserDialog from "@/components/UserDialog"
|
||||||
|
import Message from "@/components/Message"
|
||||||
|
|
||||||
ChartJS.register(
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement
|
|
||||||
)
|
|
||||||
export default {
|
export default {
|
||||||
name: "CommunicationsChat",
|
name: "CommunicationsChat",
|
||||||
components: { UserDialog, NicknameDialog, CommsInput, Chart },
|
components: { Message, UserDialog, NicknameDialog, CommsInput },
|
||||||
props: ["chat", "loading", "items"],
|
props: ["chat", "loading", "items"],
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
reachedTop: false,
|
||||||
graphOptions: {
|
graphOptions: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
|
@ -1388,7 +697,7 @@ export default {
|
||||||
display: false
|
display: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
page: 1,
|
offset: 0,
|
||||||
nickname: {
|
nickname: {
|
||||||
dialog: false,
|
dialog: false,
|
||||||
nickname: "",
|
nickname: "",
|
||||||
|
@ -1479,22 +788,51 @@ export default {
|
||||||
blobURL: "",
|
blobURL: "",
|
||||||
autoScrollRetry: 0,
|
autoScrollRetry: 0,
|
||||||
searchPanel: false,
|
searchPanel: false,
|
||||||
userPanel: true
|
userPanel: true,
|
||||||
|
rateLimit: false,
|
||||||
|
loadingMessages: true,
|
||||||
|
avoidAutoScroll: false
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
associations() {
|
associations() {
|
||||||
return this.chat.chat.associations.slice().sort((a, b) => {
|
if (this.chat) {
|
||||||
if (a.lastRead > b.lastRead) {
|
return this.chat.chat.associations.slice().sort((a, b) => {
|
||||||
return -1
|
if (a.lastRead > b.lastRead) {
|
||||||
} else if (a.lastRead < b.lastRead) {
|
return -1
|
||||||
return 1
|
} else if (a.lastRead < b.lastRead) {
|
||||||
} else {
|
return 1
|
||||||
return 0
|
} else {
|
||||||
}
|
return 0
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log("Chat could not be found (associations)")
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
scrollEvent(e) {
|
||||||
|
this.avoidAutoScroll =
|
||||||
|
e.target.scrollTop + e.target.offsetHeight !== e.target.scrollHeight
|
||||||
|
if (
|
||||||
|
e.target.scrollTop === 0 &&
|
||||||
|
!this.rateLimit &&
|
||||||
|
!this.reachedTop &&
|
||||||
|
!this.loadingMessages
|
||||||
|
) {
|
||||||
|
this.rateLimit = true
|
||||||
|
this.offset += 50
|
||||||
|
this.loadingMessages = true
|
||||||
|
this.getMessages()
|
||||||
|
setTimeout(() => {
|
||||||
|
this.rateLimit = false
|
||||||
|
}, 250)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setReply(message) {
|
||||||
|
this.replying = message
|
||||||
|
},
|
||||||
markAsRead() {
|
markAsRead() {
|
||||||
if (this.items) {
|
if (this.items) {
|
||||||
try {
|
try {
|
||||||
|
@ -1708,45 +1046,47 @@ export default {
|
||||||
this.edit.id = ""
|
this.edit.id = ""
|
||||||
this.focusInput()
|
this.focusInput()
|
||||||
},
|
},
|
||||||
autoScroll(smooth = true) {
|
autoScroll() {
|
||||||
this.$nextTick(() => {
|
if (!this.avoidAutoScroll) {
|
||||||
try {
|
this.$nextTick(() => {
|
||||||
const lastIndex = this.messages.length - 1
|
try {
|
||||||
const lastMessage = document.querySelector(`#message-${lastIndex}`)
|
const lastIndex = this.messages.length - 1
|
||||||
if (smooth) {
|
const lastMessage = document.querySelector(`#message-${lastIndex}`)
|
||||||
lastMessage.scrollIntoView({
|
|
||||||
behavior: "smooth"
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
lastMessage.scrollIntoView()
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.autoScrollRetry = 0
|
})
|
||||||
} catch (e) {
|
}
|
||||||
console.log("Could not auto scroll, retrying...")
|
|
||||||
if (this.autoScrollRetry < 20) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.autoScroll(smooth)
|
|
||||||
}, 50)
|
|
||||||
this.autoScrollRetry++
|
|
||||||
} else {
|
|
||||||
console.log("Could not auto scroll, retry limit reached")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
getMessages() {
|
getMessages() {
|
||||||
|
this.loadingMessages = true
|
||||||
this.axios
|
this.axios
|
||||||
.get(
|
.get(
|
||||||
process.env.VUE_APP_BASE_URL +
|
process.env.VUE_APP_BASE_URL +
|
||||||
"/api/v1/communications/" +
|
"/api/v1/communications/" +
|
||||||
this.$route.params.id +
|
this.$route.params.id +
|
||||||
"/messages?limit=50"
|
"/messages?limit=50&offset=" +
|
||||||
|
this.offset
|
||||||
)
|
)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.messages = res.data
|
if (!res.data.length) {
|
||||||
|
this.reachedTop = true
|
||||||
|
}
|
||||||
|
this.messages.unshift(...res.data)
|
||||||
|
this.loadingMessages = false
|
||||||
this.markRead()
|
this.markRead()
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.autoScroll(false)
|
this.autoScroll()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
@ -1764,15 +1104,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
/* // check if document.querySelector(`#message-${lastIndex}`) is scrolled to the top, and load new messages
|
document
|
||||||
window.addEventListener("scroll", () => {
|
.getElementById("message-list")
|
||||||
const lastIndex = this.messages.length - 1
|
.addEventListener("scroll", this.scrollEvent)
|
||||||
const lastMessage = document.querySelector(`#message-${lastIndex}`)
|
|
||||||
if (lastMessage && lastMessage.getBoundingClientRect().top < 0) {
|
|
||||||
this.page += 1
|
|
||||||
this.getMessages()
|
|
||||||
}
|
|
||||||
})*/
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
this.typing()
|
this.typing()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
@ -1794,8 +1128,9 @@ export default {
|
||||||
this.messages.push(message)
|
this.messages.push(message)
|
||||||
this.autoScroll()
|
this.autoScroll()
|
||||||
this.markRead()
|
this.markRead()
|
||||||
if (this.messages.length > 50) {
|
if (this.messages.length > 50 && !this.avoidAutoScroll) {
|
||||||
this.messages.shift()
|
this.messages.shift()
|
||||||
|
this.reachedTop = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1816,7 +1151,7 @@ export default {
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.messages[index].keyId = message.id + "-" + message.editedAt
|
this.messages[index].keyId = message.id + "-" + message.editedAt
|
||||||
this.messages[index].embeds = message.embeds
|
this.messages[index].embeds = message.embeds
|
||||||
this.autoScroll(true)
|
this.autoScroll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1860,6 +1195,7 @@ export default {
|
||||||
this.messages = []
|
this.messages = []
|
||||||
this.usersTyping = []
|
this.usersTyping = []
|
||||||
this.replying = null
|
this.replying = null
|
||||||
|
this.offset = 0
|
||||||
this.getMessages()
|
this.getMessages()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1258,10 +1258,10 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
||||||
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
|
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
|
||||||
|
|
||||||
"@types/mime@^1":
|
"@types/mime@*":
|
||||||
version "1.3.2"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.0.tgz#e9a9903894405c6a6551f1774df4e64d9804d69c"
|
||||||
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
|
integrity sha512-fccbsHKqFDXClBZTDLA43zl0+TbxyIwyzIzwwhvoJvhNjOErCdeX2xJbURimv2EbSVUGav001PaCJg4mZxMl4w==
|
||||||
|
|
||||||
"@types/minimatch@*":
|
"@types/minimatch@*":
|
||||||
version "3.0.5"
|
version "3.0.5"
|
||||||
|
@ -1299,11 +1299,11 @@
|
||||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||||
|
|
||||||
"@types/serve-static@*":
|
"@types/serve-static@*":
|
||||||
version "1.13.10"
|
version "1.15.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9"
|
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155"
|
||||||
integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==
|
integrity sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/mime" "^1"
|
"@types/mime" "*"
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/source-list-map@*":
|
"@types/source-list-map@*":
|
||||||
|
@ -2584,7 +2584,7 @@ browserify-zlib@^0.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pako "~1.0.5"
|
pako "~1.0.5"
|
||||||
|
|
||||||
browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.20.2, browserslist@^4.21.2:
|
browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.20.2, browserslist@^4.21.3:
|
||||||
version "4.21.3"
|
version "4.21.3"
|
||||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a"
|
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a"
|
||||||
integrity sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==
|
integrity sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==
|
||||||
|
@ -3206,11 +3206,11 @@ copyfiles@^2.4.0:
|
||||||
yargs "^16.1.0"
|
yargs "^16.1.0"
|
||||||
|
|
||||||
core-js-compat@^3.21.0, core-js-compat@^3.22.1, core-js-compat@^3.6.5:
|
core-js-compat@^3.21.0, core-js-compat@^3.22.1, core-js-compat@^3.6.5:
|
||||||
version "3.24.0"
|
version "3.24.1"
|
||||||
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.24.0.tgz#885958fac38bf3f4464a90f2663b4620f6aee6e3"
|
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.24.1.tgz#d1af84a17e18dfdd401ee39da9996f9a7ba887de"
|
||||||
integrity sha512-F+2E63X3ff/nj8uIrf8Rf24UDGIz7p838+xjEp+Bx3y8OWXj+VTPPZNCtdqovPaS9o7Tka5mCH01Zn5vOd6UQg==
|
integrity sha512-XhdNAGeRnTpp8xbD+sR/HFDK9CbeeeqXT6TuofXh3urqEevzkWmLRgrVoykodsw8okqo2pu1BOmuCKrHx63zdw==
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist "^4.21.2"
|
browserslist "^4.21.3"
|
||||||
semver "7.0.0"
|
semver "7.0.0"
|
||||||
|
|
||||||
core-js@^2.4.0:
|
core-js@^2.4.0:
|
||||||
|
@ -3219,9 +3219,9 @@ core-js@^2.4.0:
|
||||||
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
|
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
|
||||||
|
|
||||||
core-js@^3.6.5:
|
core-js@^3.6.5:
|
||||||
version "3.24.0"
|
version "3.24.1"
|
||||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.24.0.tgz#4928d4e99c593a234eb1a1f9abd3122b04d3ac57"
|
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.24.1.tgz#cf7724d41724154010a6576b7b57d94c5d66e64f"
|
||||||
integrity sha512-IeOyT8A6iK37Ep4kZDD423mpi6JfPRoPUdQwEWYiGolvn4o6j2diaRzNfDfpTdu3a5qMbrGUzKUpYpRY8jXCkQ==
|
integrity sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg==
|
||||||
|
|
||||||
core-util-is@1.0.2:
|
core-util-is@1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
|
@ -9520,9 +9520,9 @@ vuetify-loader@^1.7.0:
|
||||||
loader-utils "^2.0.0"
|
loader-utils "^2.0.0"
|
||||||
|
|
||||||
vuetify@^2.6.4:
|
vuetify@^2.6.4:
|
||||||
version "2.6.7"
|
version "2.6.8"
|
||||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-2.6.7.tgz#9c6fb7d20e1c4b07417084cbe5996c2dadf099df"
|
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-2.6.8.tgz#3f4cfa56b7eff4bad1818cd1812b47f6754e140a"
|
||||||
integrity sha512-1XxCv6mt1UsKaW7rqow0hF/jOw1ijT/fHH9euWLHgSVIcPAx8D2AY5ihTCBbMmV53Hj/YD0DUvzyk6cM/OGHvw==
|
integrity sha512-CbJsIGfye++an5/I5ypmYgf74vxt5j0NJ/7UUIDXRYXZsM9YuEpnqo97Ob4LD6QEli1gJa6WXWS8pXLWk0ArPQ==
|
||||||
|
|
||||||
vuex@^3.4.0:
|
vuex@^3.4.0:
|
||||||
version "3.6.2"
|
version "3.6.2"
|
||||||
|
|
Loading…
Reference in a new issue