mirror of
https://github.com/Troplo/Colubrina.git
synced 2024-11-22 11:17:56 +11:00
1.0.23
This commit is contained in:
parent
eaf54b2863
commit
cbe784875b
21 changed files with 5452 additions and 10834 deletions
|
@ -23,6 +23,7 @@ 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.use("/api/v1/polls", require("./routes/polls.js"))
|
||||
app.get("/api/v1/state", async (req, res) => {
|
||||
res.json({
|
||||
release: req.app.locals.config.release,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
let { Sequelize } = require("../models")
|
||||
let Errors = require("./errors")
|
||||
const multer = require("multer")
|
||||
|
||||
module.exports = function (err, req, res, next) {
|
||||
if (err instanceof Sequelize.ValidationError) {
|
||||
|
@ -10,6 +11,14 @@ module.exports = function (err, req, res, next) {
|
|||
})
|
||||
} else {
|
||||
console.error(err)
|
||||
if (err instanceof multer.MulterError) {
|
||||
if (err.code === "LIMIT_FILE_SIZE") {
|
||||
return res.status(400).json({
|
||||
errors: [Errors.fileTooLarge]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
errors: [Errors.unknown]
|
||||
})
|
||||
|
|
|
@ -29,7 +29,10 @@ let Errors = {
|
|||
"The file you are trying to upload is not a valid file type.",
|
||||
400
|
||||
],
|
||||
fileTooLarge: ["The file you are trying to upload is too large.", 400],
|
||||
fileTooLarge: [
|
||||
"The file you are trying to upload is too large. Maximum 50MB.",
|
||||
400
|
||||
],
|
||||
invalidPassword: [
|
||||
"Your password must be at least 8 characters in length.",
|
||||
400
|
||||
|
|
68
backend/migrations/20220814053827-polls.js
Normal file
68
backend/migrations/20220814053827-polls.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
"use strict"
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable("Polls", {
|
||||
id: {
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT
|
||||
},
|
||||
messageId: {
|
||||
type: Sequelize.BIGINT
|
||||
},
|
||||
userId: {
|
||||
type: Sequelize.BIGINT
|
||||
},
|
||||
title: {
|
||||
type: Sequelize.STRING
|
||||
},
|
||||
description: {
|
||||
type: Sequelize.TEXT
|
||||
},
|
||||
options: {
|
||||
type: Sequelize.JSON,
|
||||
defaultValue: []
|
||||
},
|
||||
createdAt: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE
|
||||
}
|
||||
})
|
||||
await queryInterface.addIndex("Polls", ["messageId"])
|
||||
await queryInterface.addIndex("Polls", ["userId"])
|
||||
await queryInterface.createTable("PollAnswers", {
|
||||
id: {
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT
|
||||
},
|
||||
pollId: {
|
||||
type: Sequelize.BIGINT
|
||||
},
|
||||
userId: {
|
||||
type: Sequelize.BIGINT
|
||||
},
|
||||
answer: {
|
||||
type: Sequelize.STRING
|
||||
}
|
||||
})
|
||||
await queryInterface.addIndex("PollAnswers", ["pollId"])
|
||||
await queryInterface.addIndex("PollAnswers", ["userId"])
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
/**
|
||||
* Add reverting commands here.
|
||||
*
|
||||
* Example:
|
||||
* await queryInterface.dropTable('users');
|
||||
*/
|
||||
}
|
||||
}
|
23
backend/migrations/20220814060016-pollsDates.js
Normal file
23
backend/migrations/20220814060016-pollsDates.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
"use strict"
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn("PollAnswers", "createdAt", {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false
|
||||
})
|
||||
await queryInterface.addColumn("PollAnswers", "updatedAt", {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false
|
||||
})
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
/**
|
||||
* Add reverting commands here.
|
||||
*
|
||||
* Example:
|
||||
* await queryInterface.dropTable('users');
|
||||
*/
|
||||
}
|
||||
}
|
|
@ -13,6 +13,10 @@ module.exports = (sequelize, DataTypes) => {
|
|||
foreignKey: "userId",
|
||||
as: "user"
|
||||
})
|
||||
Chat.hasOne(models.ChatAssociation, {
|
||||
foreignKey: "chatId",
|
||||
as: "association"
|
||||
})
|
||||
Chat.hasMany(models.ChatAssociation, {
|
||||
foreignKey: "chatId",
|
||||
as: "associations"
|
||||
|
|
|
@ -26,6 +26,10 @@ module.exports = (sequelize, DataTypes) => {
|
|||
as: "readReceipts",
|
||||
foreignKey: "lastRead"
|
||||
})
|
||||
Message.hasOne(models.Poll, {
|
||||
as: "poll",
|
||||
foreignKey: "messageId"
|
||||
})
|
||||
}
|
||||
}
|
||||
Message.init(
|
||||
|
|
46
backend/models/pollanswers.js
Normal file
46
backend/models/pollanswers.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
"use strict"
|
||||
const { Model } = require("sequelize")
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
class PollAnswer extends Model {
|
||||
/**
|
||||
* Helper method for defining associations.
|
||||
* This method is not a part of Sequelize lifecycle.
|
||||
* The `models/index` file will call this method automatically.
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
static associate(models) {
|
||||
PollAnswer.belongsTo(models.Poll, {
|
||||
as: "poll"
|
||||
})
|
||||
PollAnswer.belongsTo(models.User, {
|
||||
as: "user"
|
||||
})
|
||||
}
|
||||
}
|
||||
PollAnswer.init(
|
||||
{
|
||||
pollId: {
|
||||
type: DataTypes.BIGINT
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.BIGINT
|
||||
},
|
||||
answer: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
createdAt: {
|
||||
allowNull: false,
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
allowNull: false,
|
||||
type: DataTypes.DATE
|
||||
}
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: "PollAnswer"
|
||||
}
|
||||
)
|
||||
return PollAnswer
|
||||
}
|
54
backend/models/polls.js
Normal file
54
backend/models/polls.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
"use strict"
|
||||
const { Model } = require("sequelize")
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
class Poll extends Model {
|
||||
/**
|
||||
* Helper method for defining associations.
|
||||
* This method is not a part of Sequelize lifecycle.
|
||||
* The `models/index` file will call this method automatically.
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
static associate(models) {
|
||||
Poll.belongsTo(models.Message, {
|
||||
as: "message"
|
||||
})
|
||||
Poll.hasMany(models.PollAnswer, {
|
||||
as: "answers",
|
||||
foreignKey: "pollId"
|
||||
})
|
||||
}
|
||||
}
|
||||
Poll.init(
|
||||
{
|
||||
messageId: {
|
||||
type: DataTypes.BIGINT
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.BIGINT
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
options: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: []
|
||||
},
|
||||
createdAt: {
|
||||
allowNull: false,
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
updatedAt: {
|
||||
allowNull: false,
|
||||
type: DataTypes.DATE
|
||||
}
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: "Poll"
|
||||
}
|
||||
)
|
||||
return Poll
|
||||
}
|
|
@ -42,7 +42,8 @@
|
|||
"sqlite3": "^5.0.10",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"uglify-js": "^2.6.0",
|
||||
"umzug": "^3.1.1"
|
||||
"umzug": "^3.1.1",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"constantinople": "^3.1.1",
|
||||
|
|
|
@ -10,7 +10,9 @@ const {
|
|||
Message,
|
||||
Friend,
|
||||
Attachment,
|
||||
Nickname
|
||||
Nickname,
|
||||
Poll,
|
||||
PollAnswer
|
||||
} = require("../models")
|
||||
const { Op } = require("sequelize")
|
||||
const rateLimit = require("express-rate-limit")
|
||||
|
@ -19,6 +21,7 @@ const cryptoRandomString = require("crypto-random-string")
|
|||
const path = require("path")
|
||||
const fs = require("fs")
|
||||
const FileType = require("file-type")
|
||||
const { v4: uuidv4 } = require("uuid")
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 10 * 1000,
|
||||
|
@ -43,7 +46,7 @@ const storage = multer.diskStorage({
|
|||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
limits: { fileSize: 12 * 1024 * 1024 }
|
||||
limits: { fileSize: 50 * 1024 * 1024 }
|
||||
})
|
||||
|
||||
const resolveEmbeds = require("../lib/resolveEmbeds.js")
|
||||
|
@ -93,7 +96,6 @@ async function createMessage(req, type, content, association, userId) {
|
|||
attributes: [
|
||||
"username",
|
||||
"name",
|
||||
|
||||
"avatar",
|
||||
"id",
|
||||
"createdAt",
|
||||
|
@ -112,7 +114,6 @@ async function createMessage(req, type, content, association, userId) {
|
|||
attributes: [
|
||||
"username",
|
||||
"name",
|
||||
|
||||
"avatar",
|
||||
"id",
|
||||
"createdAt",
|
||||
|
@ -127,7 +128,6 @@ async function createMessage(req, type, content, association, userId) {
|
|||
attributes: [
|
||||
"username",
|
||||
"name",
|
||||
|
||||
"avatar",
|
||||
"id",
|
||||
"createdAt",
|
||||
|
@ -1369,11 +1369,61 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => {
|
|||
embeds,
|
||||
replyId: reply.id
|
||||
})
|
||||
if (req.body.embeds?.length && !req.user.bot) {
|
||||
if (req.body.embeds.length > 1) {
|
||||
throw Errors.invalidParameter("embeds", "Maximum length is 1")
|
||||
}
|
||||
for (const embed of req.body.embeds) {
|
||||
if (embed.type === "poll-v1") {
|
||||
if (!embed.title) {
|
||||
throw Errors.invalidParameter("embeds", "title is required")
|
||||
}
|
||||
if (embed.options?.length < 2) {
|
||||
throw Errors.invalidParameter("embeds", "options is required")
|
||||
}
|
||||
if (embed.options.length > 4) {
|
||||
throw Errors.invalidParameter(
|
||||
"embeds",
|
||||
"Maximum length is 4 for options"
|
||||
)
|
||||
}
|
||||
await Poll.create({
|
||||
title: embed.title,
|
||||
description: embed.description,
|
||||
userId: req.user.id,
|
||||
messageId: message.id,
|
||||
options: embed.options.map((option) => {
|
||||
return {
|
||||
value: option.toString(),
|
||||
id: uuidv4()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
const messageLookup = await Message.findOne({
|
||||
where: {
|
||||
id: message.id
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Poll,
|
||||
as: "poll",
|
||||
include: [
|
||||
{
|
||||
model: PollAnswer,
|
||||
as: "answers",
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "user",
|
||||
attributes: ["username", "avatar", "id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: ChatAssociation,
|
||||
as: "readReceipts",
|
||||
|
@ -1611,12 +1661,10 @@ router.get("/:id/messages", auth, async (req, res, next) => {
|
|||
attributes: [
|
||||
"username",
|
||||
"name",
|
||||
|
||||
"avatar",
|
||||
"id",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
|
||||
"admin",
|
||||
"bot"
|
||||
]
|
||||
|
@ -1653,6 +1701,24 @@ router.get("/:id/messages", auth, async (req, res, next) => {
|
|||
...or
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Poll,
|
||||
as: "poll",
|
||||
include: [
|
||||
{
|
||||
model: PollAnswer,
|
||||
as: "answers",
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "user",
|
||||
required: false,
|
||||
attributes: ["username", "avatar", "id"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: ChatAssociation,
|
||||
as: "readReceipts",
|
||||
|
|
|
@ -32,6 +32,9 @@ router.get("/:mid/:index/:securityToken", async (req, res, next) => {
|
|||
res.setHeader("cache-control", "public, max-age=604800")
|
||||
res.end(response.data, "binary")
|
||||
})
|
||||
.catch(() => {
|
||||
res.status(404).end()
|
||||
})
|
||||
} catch (e) {
|
||||
next(e)
|
||||
}
|
||||
|
@ -65,6 +68,9 @@ router.get("/:mid/:index/:securityToken.:extension", async (req, res, next) => {
|
|||
res.setHeader("cache-control", "public, max-age=604800")
|
||||
res.end(response.data, "binary")
|
||||
})
|
||||
.catch(() => {
|
||||
res.status(404).end()
|
||||
})
|
||||
} catch (e) {
|
||||
next(e)
|
||||
}
|
||||
|
|
112
backend/routes/polls.js
Normal file
112
backend/routes/polls.js
Normal file
|
@ -0,0 +1,112 @@
|
|||
const express = require("express")
|
||||
const router = express.Router()
|
||||
const Errors = require("../lib/errors.js")
|
||||
const {
|
||||
User,
|
||||
Chat,
|
||||
ChatAssociation,
|
||||
Poll,
|
||||
PollAnswer,
|
||||
Message
|
||||
} = require("../models")
|
||||
const auth = require("../lib/authorize")
|
||||
const rateLimit = require("express-rate-limit")
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 20 * 1000,
|
||||
max: 8,
|
||||
message: Errors.rateLimit,
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
keyGenerator: (req, res) => req.user?.id || req.ip
|
||||
})
|
||||
|
||||
router.post("/:id/vote", auth, limiter, async (req, res, next) => {
|
||||
try {
|
||||
const io = req.app.get("io")
|
||||
const poll = await Poll.findOne({
|
||||
where: {
|
||||
id: req.params.id
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Message,
|
||||
as: "message",
|
||||
include: [
|
||||
{
|
||||
model: Chat,
|
||||
as: "chat",
|
||||
include: [
|
||||
{
|
||||
model: ChatAssociation,
|
||||
as: "association",
|
||||
where: {
|
||||
userId: req.user.id
|
||||
}
|
||||
},
|
||||
{
|
||||
model: ChatAssociation,
|
||||
as: "associations"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
if (!poll) throw Errors.invalidParameter("poll id")
|
||||
let answer = await PollAnswer.findOne({
|
||||
where: {
|
||||
pollId: poll.id,
|
||||
userId: req.user.id
|
||||
}
|
||||
})
|
||||
const validate = poll.options.find(
|
||||
(option) => option.id === req.body.option
|
||||
)
|
||||
if (!validate) throw Errors.invalidParameter("option")
|
||||
if (answer) {
|
||||
if (answer?.answer === req.body.option) {
|
||||
for (const association of poll.message.chat.associations) {
|
||||
io.to(association.userId).emit(`pollAnswer-${poll.messageId}`, {
|
||||
poll: poll,
|
||||
answer: null,
|
||||
id: answer.id
|
||||
})
|
||||
}
|
||||
await answer.destroy()
|
||||
res.sendStatus(204)
|
||||
return
|
||||
}
|
||||
await answer.update({
|
||||
answer: req.body.option
|
||||
})
|
||||
for (const association of poll.message.chat.associations) {
|
||||
io.to(association.userId).emit(`pollAnswer-${poll.messageId}`, {
|
||||
poll: poll,
|
||||
answer: answer,
|
||||
id: answer.id
|
||||
})
|
||||
}
|
||||
res.sendStatus(204)
|
||||
} else {
|
||||
answer = await PollAnswer.create({
|
||||
pollId: poll.id,
|
||||
userId: req.user.id,
|
||||
answer: req.body.option
|
||||
})
|
||||
for (const association of poll.message.chat.associations) {
|
||||
io.to(association.userId).emit(`pollAnswer-${poll.messageId}`, {
|
||||
poll: poll,
|
||||
answer: answer,
|
||||
id: answer.id
|
||||
})
|
||||
}
|
||||
res.sendStatus(204)
|
||||
}
|
||||
} catch (e) {
|
||||
next(e)
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "colubrina",
|
||||
"version": "1.0.22",
|
||||
"version": "1.0.23",
|
||||
"description": "Simple instant communication.",
|
||||
"private": true,
|
||||
"author": "Troplo <troplo@troplo.com>",
|
||||
|
@ -50,9 +50,7 @@
|
|||
"vue-toastification": "^1.7.14",
|
||||
"vue2-ace-editor": "^0.0.15",
|
||||
"vuetify": "^2.6.4",
|
||||
"vuex": "^3.4.0",
|
||||
"vue-cli-plugin-electron-builder": "^2.1.1",
|
||||
"electron-devtools-installer": "^3.1.0"
|
||||
"vuex": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.16.7",
|
||||
|
@ -75,7 +73,9 @@
|
|||
"vue-template-compiler": "^2.6.11",
|
||||
"vuetify-loader": "^1.7.0",
|
||||
"webpack-auto-inject-version-next": "1.2.4",
|
||||
"webpack-bundle-analyzer": "^4.5.0"
|
||||
"webpack-bundle-analyzer": "^4.5.0",
|
||||
"vue-cli-plugin-electron-builder": "^2.1.1",
|
||||
"electron-devtools-installer": "^3.1.0"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"resolutions": {
|
||||
|
|
|
@ -1,5 +1,81 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-dialog v-model="poll.dialog" max-width="500">
|
||||
<v-card class="mb-0" color="card">
|
||||
<v-toolbar color="toolbar">
|
||||
<v-toolbar-title> Poll </v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
<v-container fluid>
|
||||
<v-text-field v-model="poll.title" label="Title"></v-text-field>
|
||||
<v-textarea
|
||||
v-model="poll.description"
|
||||
label="Description"
|
||||
></v-textarea>
|
||||
<v-text-field
|
||||
v-for="(value, index) in poll.options"
|
||||
:key="index"
|
||||
v-model="poll.options[index]"
|
||||
:label="`Option ${index + 1}`"
|
||||
:maxlength="30"
|
||||
:append-outer-icon="poll.options.length > 2 ? 'mdi-close' : ''"
|
||||
@click:append-outer="poll.options.splice(index, 1)"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
@click="poll.options.push('')"
|
||||
v-if="poll.options.length <= 3"
|
||||
text
|
||||
block
|
||||
>
|
||||
<v-icon> mdi-plus </v-icon>
|
||||
</v-btn>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="red" text @click="poll.dialog = false"> Cancel </v-btn>
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="
|
||||
createPoll()
|
||||
poll.dialog = false
|
||||
"
|
||||
>
|
||||
Add to message
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<v-toolbar
|
||||
elevation="0"
|
||||
outlined
|
||||
height="40"
|
||||
color="card"
|
||||
v-for="(embed, index) in embeds"
|
||||
style="cursor: pointer; overflow: hidden"
|
||||
class="mb-2"
|
||||
:key="index"
|
||||
>
|
||||
<v-toolbar-title>
|
||||
<v-icon> mdi-attachment </v-icon>
|
||||
{{ embedName(embed.type) }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon @click="embeds.splice(index, 1)" small>
|
||||
<v-icon> mdi-close </v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
<v-progress-linear
|
||||
v-model="uploadPercentage"
|
||||
v-if="uploading"
|
||||
height="15"
|
||||
color="toolbar"
|
||||
disabled
|
||||
class="rounded-xl"
|
||||
>
|
||||
<small>{{ uploadPercentage }}%</small>
|
||||
</v-progress-linear>
|
||||
<v-toolbar
|
||||
elevation="0"
|
||||
outlined
|
||||
|
@ -62,15 +138,110 @@
|
|||
<v-icon> mdi-send </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:prepend>
|
||||
<v-file-input
|
||||
style="margin-top: -18px"
|
||||
single-line
|
||||
hide-input
|
||||
v-model="file"
|
||||
@change="getURLForImage"
|
||||
v-if="!edit"
|
||||
></v-file-input>
|
||||
<template v-slot:prepend-inner>
|
||||
<v-menu
|
||||
:nudge-top="10"
|
||||
:nudge-left="5"
|
||||
:close-delay="100"
|
||||
:close-on-content-click="false"
|
||||
bottom
|
||||
offset-y
|
||||
top
|
||||
>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
v-on="on"
|
||||
id="attachment-button"
|
||||
icon
|
||||
style="margin-top: -2px; margin-left: -2px"
|
||||
small
|
||||
@dblclick.stop="openFileInput"
|
||||
>
|
||||
<v-icon>mdi-plus-circle</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<div>
|
||||
<v-list>
|
||||
<v-list-item @click="poll.dialog = true">
|
||||
<v-icon class="mr-2"> mdi-poll </v-icon>
|
||||
Create a poll
|
||||
</v-list-item>
|
||||
<v-list-item @click="openFileInput">
|
||||
<v-file-input
|
||||
style="margin-top: -10px"
|
||||
single-line
|
||||
hide-input
|
||||
v-model="file"
|
||||
@change="getURLForImage"
|
||||
v-if="!edit"
|
||||
ref="file-input"
|
||||
st
|
||||
></v-file-input>
|
||||
Upload a file
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</v-menu>
|
||||
<v-menu
|
||||
:nudge-top="10"
|
||||
:nudge-left="5"
|
||||
:close-delay="100"
|
||||
:close-on-content-click="false"
|
||||
bottom
|
||||
offset-y
|
||||
top
|
||||
v-if="false"
|
||||
>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
id="emoji-button"
|
||||
icon
|
||||
v-on="on"
|
||||
style="
|
||||
margin-top: -2px;
|
||||
margin-left: 1px;
|
||||
filter: grayscale(100%);
|
||||
"
|
||||
small
|
||||
@dblclick.stop="openFileInput"
|
||||
>
|
||||
<img
|
||||
style="width: 1.65em; height: 1.65em"
|
||||
class="emoji"
|
||||
draggable="false"
|
||||
alt="😀"
|
||||
src="https://twemoji.maxcdn.com/v/14.0.2/svg/1f600.svg"
|
||||
/>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card width="300" height="300">
|
||||
<v-tabs vertical height="300">
|
||||
<v-tab v-for="category in categories" :key="category">
|
||||
{{ category }}
|
||||
</v-tab>
|
||||
<v-tab-item
|
||||
v-for="category in categories"
|
||||
:key="category + '-item'"
|
||||
>
|
||||
<v-card height="300">
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col
|
||||
v-for="emoji in emojisByCategory[category]"
|
||||
:key="emoji.emoji"
|
||||
sm="4"
|
||||
v-html="twemoji(emoji.emoji)"
|
||||
@click="addEmoji(emoji.emoji)"
|
||||
style="cursor: pointer"
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
</v-tabs>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-textarea>
|
||||
</div>
|
||||
|
@ -78,6 +249,8 @@
|
|||
|
||||
<script>
|
||||
import AjaxErrorHandler from "@/lib/errorHandler"
|
||||
import twemoji from "twemoji"
|
||||
const emojis = require("../lib/emojis.json")
|
||||
|
||||
export default {
|
||||
name: "CommsInput",
|
||||
|
@ -113,7 +286,16 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
uploadPercentage: 0,
|
||||
uploading: false,
|
||||
poll: {
|
||||
dialog: false,
|
||||
title: "",
|
||||
options: ["", ""],
|
||||
description: ""
|
||||
},
|
||||
message: "",
|
||||
embeds: [],
|
||||
file: null,
|
||||
blobURL: null,
|
||||
mentions: false,
|
||||
|
@ -122,7 +304,57 @@ export default {
|
|||
completionWord: ""
|
||||
}
|
||||
},
|
||||
/*
|
||||
computed: {
|
||||
emojis() {
|
||||
return emojis
|
||||
},
|
||||
categories() {
|
||||
return this.emojis
|
||||
.map((emoji) => emoji.category)
|
||||
.filter((category, index, array) => {
|
||||
return array.indexOf(category) === index
|
||||
})
|
||||
},
|
||||
emojisByCategory() {
|
||||
return this.categories.reduce((acc, category) => {
|
||||
acc[category] = this.emojis.filter((emoji) => {
|
||||
return emoji.category === category
|
||||
})
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
},*/
|
||||
methods: {
|
||||
embedName(type) {
|
||||
if (type === "poll-v1") {
|
||||
return "Interactive Poll"
|
||||
} else if (type === "embed-v1") {
|
||||
return "Standard Embed"
|
||||
} else {
|
||||
return type
|
||||
}
|
||||
},
|
||||
createPoll() {
|
||||
this.embeds.push({
|
||||
type: "poll-v1",
|
||||
title: this.poll.title,
|
||||
options: this.poll.options,
|
||||
description: this.poll.description
|
||||
})
|
||||
},
|
||||
addEmoji(emoji) {
|
||||
this.message += emoji
|
||||
},
|
||||
twemoji(emoji) {
|
||||
return twemoji.parse(emoji, {
|
||||
folder: "svg",
|
||||
ext: ".svg"
|
||||
})
|
||||
},
|
||||
openFileInput() {
|
||||
this.$refs["file-input"].$refs.input.click()
|
||||
},
|
||||
tabCompletion() {
|
||||
if (!this.completions.length) {
|
||||
const word = this.message.split(" ").pop().toLowerCase()
|
||||
|
@ -260,7 +492,6 @@ export default {
|
|||
let message = this.message
|
||||
this.message = ""
|
||||
if (this.file || message.length > 0) {
|
||||
const emojis = require("../lib/emojis.json")
|
||||
message = message.replaceAll(
|
||||
/:([a-zA-Z0-9_\-+]+):/g,
|
||||
(match, group1) => {
|
||||
|
@ -283,25 +514,36 @@ export default {
|
|||
"/message",
|
||||
{
|
||||
message: message,
|
||||
replyId: this.replying?.id
|
||||
replyId: this.replying?.id,
|
||||
embeds: this.embeds
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
this.focusInput()
|
||||
this.autoScroll()
|
||||
this.endSend()
|
||||
this.embeds = []
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e)
|
||||
AjaxErrorHandler(this.$store)(e)
|
||||
})
|
||||
} else {
|
||||
if (this.uploading) return
|
||||
if (this.file.size > 50 * 1024 * 1024) {
|
||||
this.$toast.error(
|
||||
"The file you are trying to upload is too large. Maximum 50MB."
|
||||
)
|
||||
return
|
||||
}
|
||||
this.uploading = true
|
||||
const formData = new FormData()
|
||||
formData.append("message", message)
|
||||
if (this.replying) {
|
||||
formData.append("replyId", this.replying.id)
|
||||
}
|
||||
formData.append("file", this.file)
|
||||
formData.append("embeds", this.embeds)
|
||||
this.axios
|
||||
.post(
|
||||
process.env.VUE_APP_BASE_URL +
|
||||
|
@ -312,7 +554,14 @@ export default {
|
|||
{
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
},
|
||||
onUploadProgress: function (progressEvent) {
|
||||
this.uploadPercentage = parseInt(
|
||||
Math.round(
|
||||
(progressEvent.loaded / progressEvent.total) * 100
|
||||
)
|
||||
)
|
||||
}.bind(this)
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
|
@ -320,8 +569,13 @@ export default {
|
|||
this.autoScroll()
|
||||
this.endSend()
|
||||
this.file = null
|
||||
this.uploading = false
|
||||
this.uploadPercentage = 0
|
||||
this.embeds = []
|
||||
})
|
||||
.catch((e) => {
|
||||
this.uploading = false
|
||||
this.uploadPercentage = 0
|
||||
console.log(e)
|
||||
AjaxErrorHandler(this.$store)(e)
|
||||
})
|
||||
|
|
|
@ -36,7 +36,6 @@
|
|||
{{ message.reply.content.substring(0, 100) }}
|
||||
</v-toolbar>
|
||||
<v-list-item
|
||||
class="max-v-list-height"
|
||||
:key="message.keyId"
|
||||
:class="{
|
||||
'message-hover': hover,
|
||||
|
@ -46,7 +45,9 @@
|
|||
:dense="lastMessage"
|
||||
:id="'message-' + index"
|
||||
@contextmenu="show($event, 'message', message)"
|
||||
:style="lastMessage ? 'margin-bottom: -5px; margin-top: -5px;' : ''"
|
||||
:style="
|
||||
lastMessage ? 'margin-bottom: -10px; margin-top: -10px;' : ''
|
||||
"
|
||||
>
|
||||
<v-avatar size="45" class="mr-2" v-if="!lastMessage">
|
||||
<v-img
|
||||
|
@ -130,6 +131,47 @@
|
|||
no-gutters
|
||||
>
|
||||
<v-card
|
||||
:min-width="!$vuetify.breakpoint.mobile ? 400 : 0"
|
||||
elevation="0"
|
||||
color="card"
|
||||
v-if="embed.type === 'image'"
|
||||
>
|
||||
<v-hover v-slot="{ hover }">
|
||||
<div>
|
||||
<v-img
|
||||
@click="setImagePreview(embed)"
|
||||
contain
|
||||
:max-width="500"
|
||||
:max-height="500"
|
||||
:src="$store.state.baseURL + embed.mediaProxyLink"
|
||||
>
|
||||
<template v-slot:placeholder>
|
||||
<v-row
|
||||
class="fill-height ma-0"
|
||||
align="center"
|
||||
justify="center"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
width="500"
|
||||
height="500"
|
||||
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>
|
||||
<v-card
|
||||
v-if="embed.type !== 'image'"
|
||||
elevation="0"
|
||||
:color="
|
||||
embed.type === 'embed-v1' ? embed.backgroundColor : 'bg'
|
||||
|
@ -186,36 +228,6 @@
|
|||
</p>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-else-if="embed.type === 'image'">
|
||||
<v-hover v-slot="{ hover }">
|
||||
<v-img
|
||||
@click="setImagePreview(embed)"
|
||||
contain
|
||||
:aspect-ratio="16 / 9"
|
||||
:src="$store.state.baseURL + 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>
|
||||
</v-hover>
|
||||
</template>
|
||||
<v-row v-else-if="embed.type === 'embed-v1'">
|
||||
<v-col
|
||||
cols="12"
|
||||
|
@ -306,6 +318,59 @@
|
|||
</v-container>
|
||||
</v-card>
|
||||
</v-row>
|
||||
<v-row v-if="message.poll" no-gutters>
|
||||
<v-card
|
||||
elevation="0"
|
||||
:max-width="500"
|
||||
:min-width="!$vuetify.breakpoint.mobile ? 400 : 200"
|
||||
class="ml-1 mb-1 mr-1 rounded-l"
|
||||
color="card lighten-1"
|
||||
>
|
||||
<v-toolbar color="toolbar" height="45">
|
||||
<v-toolbar-title>
|
||||
Poll: {{ message.poll.title }}
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card-text>
|
||||
{{ message.poll.description }}
|
||||
<v-progress-linear
|
||||
v-for="option in message.poll.options"
|
||||
:key="option.id"
|
||||
block
|
||||
class="mb-1 rounded-xl"
|
||||
height="30"
|
||||
text
|
||||
:value="
|
||||
percentageVotes.find(
|
||||
(percentage) => percentage.id === option.id
|
||||
).percentage
|
||||
"
|
||||
color="success darken-1"
|
||||
background-opacity="0.2"
|
||||
outlined
|
||||
style="text-transform: none; cursor: pointer"
|
||||
@click="votePoll(option.id)"
|
||||
>
|
||||
<span style="float: left !important">
|
||||
<v-icon v-if="option.id === myVote?.answer">
|
||||
mdi-check-circle
|
||||
</v-icon>
|
||||
{{ option.value }} ({{
|
||||
percentageVotes.find(
|
||||
(percentage) => percentage.id === option.id
|
||||
).percentage
|
||||
}}% /
|
||||
{{
|
||||
message.poll.answers.filter(
|
||||
(answer) => answer.answer === option.id
|
||||
)?.length || 0
|
||||
}})
|
||||
</span>
|
||||
</v-progress-linear>
|
||||
{{ message.poll.answers.length }} votes
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-row>
|
||||
</template>
|
||||
<template v-if="edit.id !== message.id">
|
||||
<v-card
|
||||
|
@ -330,7 +395,8 @@
|
|||
<v-img
|
||||
@click="setImagePreview(attachment)"
|
||||
contain
|
||||
:aspect-ratio="16 / 9"
|
||||
:max-width="500"
|
||||
:max-height="500"
|
||||
:src="
|
||||
$store.state.baseURL +
|
||||
'/usercontent/' +
|
||||
|
@ -345,6 +411,8 @@
|
|||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
width="500"
|
||||
height="500"
|
||||
color="grey lighten-5"
|
||||
></v-progress-circular>
|
||||
</v-row>
|
||||
|
@ -899,6 +967,24 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
myVote() {
|
||||
return this.message.poll.answers.find(
|
||||
(vote) => vote.userId === this.$store.state.user.id
|
||||
)
|
||||
},
|
||||
percentageVotes() {
|
||||
return this.message.poll.options.map((option) => {
|
||||
return {
|
||||
id: option.id,
|
||||
percentage:
|
||||
((this.message.poll.answers?.filter(
|
||||
(answer) => answer?.answer === option.id
|
||||
).length || 0) /
|
||||
this.message.poll.answers.length) *
|
||||
100 || 0
|
||||
}
|
||||
})
|
||||
},
|
||||
mentioned() {
|
||||
return this.message.content
|
||||
.toLowerCase()
|
||||
|
@ -906,6 +992,15 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
votePoll(option) {
|
||||
this.axios
|
||||
.post(`/api/v1/polls/${this.message.poll.id}/vote`, {
|
||||
option
|
||||
})
|
||||
.catch((e) => {
|
||||
AjaxErrorHandler(this.$store)(e)
|
||||
})
|
||||
},
|
||||
pinMessage() {
|
||||
this.axios
|
||||
.post(`/api/v1/communications/${this.chat.id}/pins`, {
|
||||
|
@ -929,6 +1024,18 @@ export default {
|
|||
return (size / 1073741824).toFixed(2) + " GB"
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.message.poll) {
|
||||
this.$socket.on(`pollAnswer-${this.message.id}`, (data) => {
|
||||
this.message.poll.answers = this.message.poll.answers.filter(
|
||||
(answer) => answer.id !== data.id
|
||||
)
|
||||
if (data.answer) {
|
||||
this.message.poll.answers.push(data.answer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -74,9 +74,16 @@ export default new Vuex.Store({
|
|||
wsRegistered: false,
|
||||
lastChat: "friends",
|
||||
searchPanel: false,
|
||||
userPanel: false
|
||||
userPanel: false,
|
||||
messages: {}
|
||||
},
|
||||
mutations: {
|
||||
setMessages(state, { id, messages }) {
|
||||
state.messages[id] = messages
|
||||
},
|
||||
appendMessage(state, { id, message }) {
|
||||
state.messages[id].push(message)
|
||||
},
|
||||
setSelectedChat(state, chat) {
|
||||
state.selectedChat = chat
|
||||
},
|
||||
|
|
|
@ -23,14 +23,6 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.$route.params.id) {
|
||||
this.$router.push("/communications/" + this.$store.state.lastChat)
|
||||
} else {
|
||||
this.$store.commit("setLastChat", this.$route.params.id || "friends")
|
||||
}
|
||||
this.$store.commit("setSelectedChat", this.selectedChat)
|
||||
},
|
||||
watch: {
|
||||
selectedChat() {
|
||||
this.$store.commit("setSelectedChat", this.selectedChat)
|
||||
|
|
|
@ -97,7 +97,6 @@
|
|||
:max-width="1000"
|
||||
:max-height="600"
|
||||
:min-height="300"
|
||||
aspect-ratio="16/9"
|
||||
contain
|
||||
></v-img>
|
||||
<v-container>
|
||||
|
@ -1065,7 +1064,11 @@ export default {
|
|||
})
|
||||
},
|
||||
async getMessages() {
|
||||
this.loadingMessages = true
|
||||
if (!this.$store.state.messages[this.chat.id]) {
|
||||
this.loadingMessages = true
|
||||
} else {
|
||||
this.autoScroll()
|
||||
}
|
||||
await this.axios
|
||||
.get(
|
||||
process.env.VUE_APP_BASE_URL +
|
||||
|
@ -1079,6 +1082,10 @@ export default {
|
|||
this.reachedTop = true
|
||||
}
|
||||
this.messages.unshift(...res.data)
|
||||
/* this.$store.commit("setMessages", {
|
||||
id: this.chat.id,
|
||||
messages: this.messages
|
||||
})*/
|
||||
this.loadingMessages = false
|
||||
this.markRead()
|
||||
this.$nextTick(() => {
|
||||
|
@ -1229,7 +1236,7 @@ export default {
|
|||
drafts[oldVal] = ""
|
||||
}
|
||||
this.message = drafts[val] || ""
|
||||
this.messages = []
|
||||
this.messages = this.$store.state.messages[val] || []
|
||||
this.usersTyping = []
|
||||
this.replying = null
|
||||
this.reachedTop = false
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
</v-toolbar>
|
||||
<v-card color="card" elevation="0">
|
||||
<v-list color="card">
|
||||
<v-list-item v-if="computePendingIncoming.length === 0">
|
||||
<v-list-item v-if="!computePendingIncoming.length">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
You currently do not have any incoming friend requests.
|
||||
|
@ -141,7 +141,7 @@
|
|||
</v-toolbar>
|
||||
<v-card color="card" elevation="0">
|
||||
<v-list color="card">
|
||||
<v-list-item v-if="computePendingOutgoing.length === 0">
|
||||
<v-list-item v-if="!computePendingOutgoing.length">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
You currently do not have any outgoing friend requests.
|
||||
|
@ -191,7 +191,7 @@
|
|||
</v-toolbar>
|
||||
<v-card color="card" elevation="0">
|
||||
<v-list color="card">
|
||||
<v-list-item v-if="computeAccepted.length === 0">
|
||||
<v-list-item v-if="!computeAccepted.length">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
You currently do not have any friends.
|
||||
|
@ -374,7 +374,6 @@ export default {
|
|||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.$store.dispatch("doInit")
|
||||
this.getFriends()
|
||||
this.getUsers()
|
||||
this.$socket.on("friendRequest", () => {
|
||||
|
|
Loading…
Reference in a new issue