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] Admin panel
|
||||
- [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] Group creation and modification
|
||||
- [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("/api/v1/usercontent", require("./routes/usercontent.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) => {
|
||||
res.json({
|
||||
release: process.env.RELEASE,
|
||||
|
|
|
@ -98,6 +98,8 @@ module.exports = {
|
|||
}
|
||||
})
|
||||
socket.on("disconnect", async function () {
|
||||
const clients = io.sockets.adapter.rooms.get(user.id) || new Set()
|
||||
if (!clients.size || clients.size === 0) {
|
||||
friends.forEach((friend) => {
|
||||
io.to(friend.friendId).emit("userStatus", {
|
||||
userId: user.id,
|
||||
|
@ -107,6 +109,7 @@ module.exports = {
|
|||
await user.update({
|
||||
status: "offline"
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
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) => {
|
||||
try {
|
||||
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) => {
|
||||
try {
|
||||
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"]],
|
||||
limit: 50
|
||||
})
|
||||
|
@ -2096,10 +1574,10 @@ router.post("/create", auth, async (req, res, next) => {
|
|||
type
|
||||
})
|
||||
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
|
||||
if (type === "group") {
|
||||
if (req.body.users[i] === req.user.id) {
|
||||
if (id === req.user.id) {
|
||||
rank = "admin"
|
||||
} else {
|
||||
rank = "member"
|
||||
|
@ -2107,14 +1585,17 @@ router.post("/create", auth, async (req, res, next) => {
|
|||
} else {
|
||||
rank = "member"
|
||||
}
|
||||
const c1 = await ChatAssociation.create({
|
||||
await ChatAssociation.create({
|
||||
chatId: chat.id,
|
||||
userId: req.body.users[i],
|
||||
userId: id,
|
||||
rank
|
||||
})
|
||||
}
|
||||
for (const id of req.body.users) {
|
||||
const association = await ChatAssociation.findOne({
|
||||
where: {
|
||||
id: c1.id
|
||||
chatId: chat.id,
|
||||
userId: id
|
||||
},
|
||||
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({
|
||||
where: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "colubrina-chat",
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.5",
|
||||
"private": true,
|
||||
"author": "Troplo <troplo@troplo.com>",
|
||||
"license": "GPL-3.0",
|
||||
|
|
|
@ -742,7 +742,7 @@ export default {
|
|||
removeUserFromGroup(user) {
|
||||
this.axios
|
||||
.delete(
|
||||
"/api/v1/communications/association/" +
|
||||
"/api/v1/association/" +
|
||||
this.settings.item.id +
|
||||
"/" +
|
||||
user.id
|
||||
|
@ -757,7 +757,7 @@ export default {
|
|||
giveUserAdmin(user) {
|
||||
this.axios
|
||||
.put(
|
||||
"/api/v1/communications/association/" +
|
||||
"/api/v1/association/" +
|
||||
this.settings.item.id +
|
||||
"/" +
|
||||
user.id,
|
||||
|
@ -798,7 +798,7 @@ export default {
|
|||
addMembersToGroup() {
|
||||
this.axios
|
||||
.post(
|
||||
"/api/v1/communications/association/" + this.settings.item.chat.id,
|
||||
"/api/v1/association/" + this.settings.item.chat.id,
|
||||
{
|
||||
users: this.settings.addMembers.users
|
||||
}
|
||||
|
@ -817,7 +817,7 @@ export default {
|
|||
},
|
||||
leaveGroup() {
|
||||
this.axios
|
||||
.delete("/api/v1/communications/association/" + this.leave.item.id)
|
||||
.delete("/api/v1/association/" + this.leave.item.id)
|
||||
.then(() => {
|
||||
this.leave.dialog = false
|
||||
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
|
||||
xhtmlOut: false, // Use '/' to close single tags (<br />).
|
||||
// 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
|
||||
// useful for external highlighters.
|
||||
linkify: true, // Autoconvert URL-like text to links
|
||||
|
|
|
@ -22,7 +22,7 @@ export default {
|
|||
selectedChat() {
|
||||
try {
|
||||
return this.$store.state.chats.find(
|
||||
(item) => item.id === JSON.parse(this.$route.params.id)
|
||||
(item) => item.id === parseInt(this.$route.params.id)
|
||||
)
|
||||
} catch {
|
||||
return null
|
||||
|
|
|
@ -125,710 +125,37 @@
|
|||
color="card"
|
||||
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-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-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
|
||||
<Message
|
||||
: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
|
||||
:message="message"
|
||||
:jump-to-message="jumpToMessage"
|
||||
:edit="edit"
|
||||
:chat="chat"
|
||||
:auto-scroll="autoScroll"
|
||||
:focus-input="focusInput"
|
||||
:replying="setReply"
|
||||
:get-name="getName"
|
||||
: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>
|
||||
:auto-scroll="autoScroll"
|
||||
:chat="chat"
|
||||
:index="index"
|
||||
:show="show"
|
||||
></Message>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
@ -1353,34 +680,16 @@
|
|||
<script>
|
||||
import AjaxErrorHandler from "@/lib/errorHandler"
|
||||
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 UserDialog from "@/components/UserDialog"
|
||||
import Message from "@/components/Message"
|
||||
|
||||
ChartJS.register(
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement
|
||||
)
|
||||
export default {
|
||||
name: "CommunicationsChat",
|
||||
components: { UserDialog, NicknameDialog, CommsInput, Chart },
|
||||
components: { Message, UserDialog, NicknameDialog, CommsInput },
|
||||
props: ["chat", "loading", "items"],
|
||||
data: () => ({
|
||||
reachedTop: false,
|
||||
graphOptions: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
|
@ -1388,7 +697,7 @@ export default {
|
|||
display: false
|
||||
}
|
||||
},
|
||||
page: 1,
|
||||
offset: 0,
|
||||
nickname: {
|
||||
dialog: false,
|
||||
nickname: "",
|
||||
|
@ -1479,10 +788,14 @@ export default {
|
|||
blobURL: "",
|
||||
autoScrollRetry: 0,
|
||||
searchPanel: false,
|
||||
userPanel: true
|
||||
userPanel: true,
|
||||
rateLimit: false,
|
||||
loadingMessages: true,
|
||||
avoidAutoScroll: false
|
||||
}),
|
||||
computed: {
|
||||
associations() {
|
||||
if (this.chat) {
|
||||
return this.chat.chat.associations.slice().sort((a, b) => {
|
||||
if (a.lastRead > b.lastRead) {
|
||||
return -1
|
||||
|
@ -1492,9 +805,34 @@ export default {
|
|||
return 0
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.log("Chat could not be found (associations)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
},
|
||||
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() {
|
||||
if (this.items) {
|
||||
try {
|
||||
|
@ -1708,24 +1046,19 @@ export default {
|
|||
this.edit.id = ""
|
||||
this.focusInput()
|
||||
},
|
||||
autoScroll(smooth = true) {
|
||||
autoScroll() {
|
||||
if (!this.avoidAutoScroll) {
|
||||
this.$nextTick(() => {
|
||||
try {
|
||||
const lastIndex = this.messages.length - 1
|
||||
const lastMessage = document.querySelector(`#message-${lastIndex}`)
|
||||
if (smooth) {
|
||||
lastMessage.scrollIntoView({
|
||||
behavior: "smooth"
|
||||
})
|
||||
} else {
|
||||
lastMessage.scrollIntoView()
|
||||
}
|
||||
this.autoScrollRetry = 0
|
||||
} catch (e) {
|
||||
console.log("Could not auto scroll, retrying...")
|
||||
if (this.autoScrollRetry < 20) {
|
||||
setTimeout(() => {
|
||||
this.autoScroll(smooth)
|
||||
this.autoScroll()
|
||||
}, 50)
|
||||
this.autoScrollRetry++
|
||||
} else {
|
||||
|
@ -1733,20 +1066,27 @@ export default {
|
|||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
getMessages() {
|
||||
this.loadingMessages = true
|
||||
this.axios
|
||||
.get(
|
||||
process.env.VUE_APP_BASE_URL +
|
||||
"/api/v1/communications/" +
|
||||
this.$route.params.id +
|
||||
"/messages?limit=50"
|
||||
"/messages?limit=50&offset=" +
|
||||
this.offset
|
||||
)
|
||||
.then((res) => {
|
||||
this.messages = res.data
|
||||
if (!res.data.length) {
|
||||
this.reachedTop = true
|
||||
}
|
||||
this.messages.unshift(...res.data)
|
||||
this.loadingMessages = false
|
||||
this.markRead()
|
||||
this.$nextTick(() => {
|
||||
this.autoScroll(false)
|
||||
this.autoScroll()
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
|
@ -1764,15 +1104,9 @@ export default {
|
|||
}
|
||||
},
|
||||
mounted() {
|
||||
/* // check if document.querySelector(`#message-${lastIndex}`) is scrolled to the top, and load new messages
|
||||
window.addEventListener("scroll", () => {
|
||||
const lastIndex = this.messages.length - 1
|
||||
const lastMessage = document.querySelector(`#message-${lastIndex}`)
|
||||
if (lastMessage && lastMessage.getBoundingClientRect().top < 0) {
|
||||
this.page += 1
|
||||
this.getMessages()
|
||||
}
|
||||
})*/
|
||||
document
|
||||
.getElementById("message-list")
|
||||
.addEventListener("scroll", this.scrollEvent)
|
||||
setInterval(() => {
|
||||
this.typing()
|
||||
}, 1000)
|
||||
|
@ -1794,8 +1128,9 @@ export default {
|
|||
this.messages.push(message)
|
||||
this.autoScroll()
|
||||
this.markRead()
|
||||
if (this.messages.length > 50) {
|
||||
if (this.messages.length > 50 && !this.avoidAutoScroll) {
|
||||
this.messages.shift()
|
||||
this.reachedTop = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1816,7 +1151,7 @@ export default {
|
|||
if (index !== -1) {
|
||||
this.messages[index].keyId = message.id + "-" + message.editedAt
|
||||
this.messages[index].embeds = message.embeds
|
||||
this.autoScroll(true)
|
||||
this.autoScroll()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1860,6 +1195,7 @@ export default {
|
|||
this.messages = []
|
||||
this.usersTyping = []
|
||||
this.replying = null
|
||||
this.offset = 0
|
||||
this.getMessages()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1258,10 +1258,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
||||
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
|
||||
|
||||
"@types/mime@^1":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
||||
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
|
||||
"@types/mime@*":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.0.tgz#e9a9903894405c6a6551f1774df4e64d9804d69c"
|
||||
integrity sha512-fccbsHKqFDXClBZTDLA43zl0+TbxyIwyzIzwwhvoJvhNjOErCdeX2xJbURimv2EbSVUGav001PaCJg4mZxMl4w==
|
||||
|
||||
"@types/minimatch@*":
|
||||
version "3.0.5"
|
||||
|
@ -1299,11 +1299,11 @@
|
|||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||
|
||||
"@types/serve-static@*":
|
||||
version "1.13.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9"
|
||||
integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==
|
||||
version "1.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155"
|
||||
integrity sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==
|
||||
dependencies:
|
||||
"@types/mime" "^1"
|
||||
"@types/mime" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/source-list-map@*":
|
||||
|
@ -2584,7 +2584,7 @@ browserify-zlib@^0.2.0:
|
|||
dependencies:
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a"
|
||||
integrity sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==
|
||||
|
@ -3206,11 +3206,11 @@ copyfiles@^2.4.0:
|
|||
yargs "^16.1.0"
|
||||
|
||||
core-js-compat@^3.21.0, core-js-compat@^3.22.1, core-js-compat@^3.6.5:
|
||||
version "3.24.0"
|
||||
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.24.0.tgz#885958fac38bf3f4464a90f2663b4620f6aee6e3"
|
||||
integrity sha512-F+2E63X3ff/nj8uIrf8Rf24UDGIz7p838+xjEp+Bx3y8OWXj+VTPPZNCtdqovPaS9o7Tka5mCH01Zn5vOd6UQg==
|
||||
version "3.24.1"
|
||||
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.24.1.tgz#d1af84a17e18dfdd401ee39da9996f9a7ba887de"
|
||||
integrity sha512-XhdNAGeRnTpp8xbD+sR/HFDK9CbeeeqXT6TuofXh3urqEevzkWmLRgrVoykodsw8okqo2pu1BOmuCKrHx63zdw==
|
||||
dependencies:
|
||||
browserslist "^4.21.2"
|
||||
browserslist "^4.21.3"
|
||||
semver "7.0.0"
|
||||
|
||||
core-js@^2.4.0:
|
||||
|
@ -3219,9 +3219,9 @@ core-js@^2.4.0:
|
|||
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
|
||||
|
||||
core-js@^3.6.5:
|
||||
version "3.24.0"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.24.0.tgz#4928d4e99c593a234eb1a1f9abd3122b04d3ac57"
|
||||
integrity sha512-IeOyT8A6iK37Ep4kZDD423mpi6JfPRoPUdQwEWYiGolvn4o6j2diaRzNfDfpTdu3a5qMbrGUzKUpYpRY8jXCkQ==
|
||||
version "3.24.1"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.24.1.tgz#cf7724d41724154010a6576b7b57d94c5d66e64f"
|
||||
integrity sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg==
|
||||
|
||||
core-util-is@1.0.2:
|
||||
version "1.0.2"
|
||||
|
@ -9520,9 +9520,9 @@ vuetify-loader@^1.7.0:
|
|||
loader-utils "^2.0.0"
|
||||
|
||||
vuetify@^2.6.4:
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-2.6.7.tgz#9c6fb7d20e1c4b07417084cbe5996c2dadf099df"
|
||||
integrity sha512-1XxCv6mt1UsKaW7rqow0hF/jOw1ijT/fHH9euWLHgSVIcPAx8D2AY5ihTCBbMmV53Hj/YD0DUvzyk6cM/OGHvw==
|
||||
version "2.6.8"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-2.6.8.tgz#3f4cfa56b7eff4bad1818cd1812b47f6754e140a"
|
||||
integrity sha512-CbJsIGfye++an5/I5ypmYgf74vxt5j0NJ/7UUIDXRYXZsM9YuEpnqo97Ob4LD6QEli1gJa6WXWS8pXLWk0ArPQ==
|
||||
|
||||
vuex@^3.4.0:
|
||||
version "3.6.2"
|
||||
|
|
Loading…
Reference in a new issue