mirror of
https://github.com/Troplo/Colubrina.git
synced 2024-11-22 19:27:55 +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/usercontent", require("./routes/usercontent.js"))
|
||||||
app.use("/api/v1/mediaproxy", require("./routes/mediaproxy.js"))
|
app.use("/api/v1/mediaproxy", require("./routes/mediaproxy.js"))
|
||||||
app.use("/api/v1/associations", require("./routes/associations.js"))
|
app.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) => {
|
app.get("/api/v1/state", async (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
release: req.app.locals.config.release,
|
release: req.app.locals.config.release,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
let { Sequelize } = require("../models")
|
let { Sequelize } = require("../models")
|
||||||
let Errors = require("./errors")
|
let Errors = require("./errors")
|
||||||
|
const multer = require("multer")
|
||||||
|
|
||||||
module.exports = function (err, req, res, next) {
|
module.exports = function (err, req, res, next) {
|
||||||
if (err instanceof Sequelize.ValidationError) {
|
if (err instanceof Sequelize.ValidationError) {
|
||||||
|
@ -10,6 +11,14 @@ module.exports = function (err, req, res, next) {
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
console.error(err)
|
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({
|
res.status(500).json({
|
||||||
errors: [Errors.unknown]
|
errors: [Errors.unknown]
|
||||||
})
|
})
|
||||||
|
|
|
@ -29,7 +29,10 @@ let Errors = {
|
||||||
"The file you are trying to upload is not a valid file type.",
|
"The file you are trying to upload is not a valid file type.",
|
||||||
400
|
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: [
|
invalidPassword: [
|
||||||
"Your password must be at least 8 characters in length.",
|
"Your password must be at least 8 characters in length.",
|
||||||
400
|
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",
|
foreignKey: "userId",
|
||||||
as: "user"
|
as: "user"
|
||||||
})
|
})
|
||||||
|
Chat.hasOne(models.ChatAssociation, {
|
||||||
|
foreignKey: "chatId",
|
||||||
|
as: "association"
|
||||||
|
})
|
||||||
Chat.hasMany(models.ChatAssociation, {
|
Chat.hasMany(models.ChatAssociation, {
|
||||||
foreignKey: "chatId",
|
foreignKey: "chatId",
|
||||||
as: "associations"
|
as: "associations"
|
||||||
|
|
|
@ -26,6 +26,10 @@ module.exports = (sequelize, DataTypes) => {
|
||||||
as: "readReceipts",
|
as: "readReceipts",
|
||||||
foreignKey: "lastRead"
|
foreignKey: "lastRead"
|
||||||
})
|
})
|
||||||
|
Message.hasOne(models.Poll, {
|
||||||
|
as: "poll",
|
||||||
|
foreignKey: "messageId"
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message.init(
|
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",
|
"sqlite3": "^5.0.10",
|
||||||
"ua-parser-js": "^1.0.2",
|
"ua-parser-js": "^1.0.2",
|
||||||
"uglify-js": "^2.6.0",
|
"uglify-js": "^2.6.0",
|
||||||
"umzug": "^3.1.1"
|
"umzug": "^3.1.1",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"constantinople": "^3.1.1",
|
"constantinople": "^3.1.1",
|
||||||
|
|
|
@ -10,7 +10,9 @@ const {
|
||||||
Message,
|
Message,
|
||||||
Friend,
|
Friend,
|
||||||
Attachment,
|
Attachment,
|
||||||
Nickname
|
Nickname,
|
||||||
|
Poll,
|
||||||
|
PollAnswer
|
||||||
} = require("../models")
|
} = require("../models")
|
||||||
const { Op } = require("sequelize")
|
const { Op } = require("sequelize")
|
||||||
const rateLimit = require("express-rate-limit")
|
const rateLimit = require("express-rate-limit")
|
||||||
|
@ -19,6 +21,7 @@ const cryptoRandomString = require("crypto-random-string")
|
||||||
const path = require("path")
|
const path = require("path")
|
||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
const FileType = require("file-type")
|
const FileType = require("file-type")
|
||||||
|
const { v4: uuidv4 } = require("uuid")
|
||||||
|
|
||||||
const limiter = rateLimit({
|
const limiter = rateLimit({
|
||||||
windowMs: 10 * 1000,
|
windowMs: 10 * 1000,
|
||||||
|
@ -43,7 +46,7 @@ const storage = multer.diskStorage({
|
||||||
|
|
||||||
const upload = multer({
|
const upload = multer({
|
||||||
storage: storage,
|
storage: storage,
|
||||||
limits: { fileSize: 12 * 1024 * 1024 }
|
limits: { fileSize: 50 * 1024 * 1024 }
|
||||||
})
|
})
|
||||||
|
|
||||||
const resolveEmbeds = require("../lib/resolveEmbeds.js")
|
const resolveEmbeds = require("../lib/resolveEmbeds.js")
|
||||||
|
@ -93,7 +96,6 @@ async function createMessage(req, type, content, association, userId) {
|
||||||
attributes: [
|
attributes: [
|
||||||
"username",
|
"username",
|
||||||
"name",
|
"name",
|
||||||
|
|
||||||
"avatar",
|
"avatar",
|
||||||
"id",
|
"id",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
|
@ -112,7 +114,6 @@ async function createMessage(req, type, content, association, userId) {
|
||||||
attributes: [
|
attributes: [
|
||||||
"username",
|
"username",
|
||||||
"name",
|
"name",
|
||||||
|
|
||||||
"avatar",
|
"avatar",
|
||||||
"id",
|
"id",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
|
@ -127,7 +128,6 @@ async function createMessage(req, type, content, association, userId) {
|
||||||
attributes: [
|
attributes: [
|
||||||
"username",
|
"username",
|
||||||
"name",
|
"name",
|
||||||
|
|
||||||
"avatar",
|
"avatar",
|
||||||
"id",
|
"id",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
|
@ -1369,11 +1369,61 @@ router.post("/:id/message", auth, limiter, async (req, res, next) => {
|
||||||
embeds,
|
embeds,
|
||||||
replyId: reply.id
|
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({
|
const messageLookup = await Message.findOne({
|
||||||
where: {
|
where: {
|
||||||
id: message.id
|
id: message.id
|
||||||
},
|
},
|
||||||
include: [
|
include: [
|
||||||
|
{
|
||||||
|
model: Poll,
|
||||||
|
as: "poll",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: PollAnswer,
|
||||||
|
as: "answers",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "user",
|
||||||
|
attributes: ["username", "avatar", "id"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
model: ChatAssociation,
|
model: ChatAssociation,
|
||||||
as: "readReceipts",
|
as: "readReceipts",
|
||||||
|
@ -1611,12 +1661,10 @@ router.get("/:id/messages", auth, async (req, res, next) => {
|
||||||
attributes: [
|
attributes: [
|
||||||
"username",
|
"username",
|
||||||
"name",
|
"name",
|
||||||
|
|
||||||
"avatar",
|
"avatar",
|
||||||
"id",
|
"id",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
"updatedAt",
|
"updatedAt",
|
||||||
|
|
||||||
"admin",
|
"admin",
|
||||||
"bot"
|
"bot"
|
||||||
]
|
]
|
||||||
|
@ -1653,6 +1701,24 @@ router.get("/:id/messages", auth, async (req, res, next) => {
|
||||||
...or
|
...or
|
||||||
},
|
},
|
||||||
include: [
|
include: [
|
||||||
|
{
|
||||||
|
model: Poll,
|
||||||
|
as: "poll",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: PollAnswer,
|
||||||
|
as: "answers",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: User,
|
||||||
|
as: "user",
|
||||||
|
required: false,
|
||||||
|
attributes: ["username", "avatar", "id"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
model: ChatAssociation,
|
model: ChatAssociation,
|
||||||
as: "readReceipts",
|
as: "readReceipts",
|
||||||
|
|
|
@ -32,6 +32,9 @@ router.get("/:mid/:index/:securityToken", async (req, res, next) => {
|
||||||
res.setHeader("cache-control", "public, max-age=604800")
|
res.setHeader("cache-control", "public, max-age=604800")
|
||||||
res.end(response.data, "binary")
|
res.end(response.data, "binary")
|
||||||
})
|
})
|
||||||
|
.catch(() => {
|
||||||
|
res.status(404).end()
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(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.setHeader("cache-control", "public, max-age=604800")
|
||||||
res.end(response.data, "binary")
|
res.end(response.data, "binary")
|
||||||
})
|
})
|
||||||
|
.catch(() => {
|
||||||
|
res.status(404).end()
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(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",
|
"name": "colubrina",
|
||||||
"version": "1.0.22",
|
"version": "1.0.23",
|
||||||
"description": "Simple instant communication.",
|
"description": "Simple instant communication.",
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": "Troplo <troplo@troplo.com>",
|
"author": "Troplo <troplo@troplo.com>",
|
||||||
|
@ -50,9 +50,7 @@
|
||||||
"vue-toastification": "^1.7.14",
|
"vue-toastification": "^1.7.14",
|
||||||
"vue2-ace-editor": "^0.0.15",
|
"vue2-ace-editor": "^0.0.15",
|
||||||
"vuetify": "^2.6.4",
|
"vuetify": "^2.6.4",
|
||||||
"vuex": "^3.4.0",
|
"vuex": "^3.4.0"
|
||||||
"vue-cli-plugin-electron-builder": "^2.1.1",
|
|
||||||
"electron-devtools-installer": "^3.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.16.7",
|
"@babel/plugin-proposal-optional-chaining": "^7.16.7",
|
||||||
|
@ -75,7 +73,9 @@
|
||||||
"vue-template-compiler": "^2.6.11",
|
"vue-template-compiler": "^2.6.11",
|
||||||
"vuetify-loader": "^1.7.0",
|
"vuetify-loader": "^1.7.0",
|
||||||
"webpack-auto-inject-version-next": "1.2.4",
|
"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",
|
"license": "GPL-3.0",
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|
|
@ -1,5 +1,81 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<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
|
<v-toolbar
|
||||||
elevation="0"
|
elevation="0"
|
||||||
outlined
|
outlined
|
||||||
|
@ -62,15 +138,110 @@
|
||||||
<v-icon> mdi-send </v-icon>
|
<v-icon> mdi-send </v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend-inner>
|
||||||
<v-file-input
|
<v-menu
|
||||||
style="margin-top: -18px"
|
:nudge-top="10"
|
||||||
single-line
|
:nudge-left="5"
|
||||||
hide-input
|
:close-delay="100"
|
||||||
v-model="file"
|
:close-on-content-click="false"
|
||||||
@change="getURLForImage"
|
bottom
|
||||||
v-if="!edit"
|
offset-y
|
||||||
></v-file-input>
|
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>
|
</template>
|
||||||
</v-textarea>
|
</v-textarea>
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,6 +249,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import AjaxErrorHandler from "@/lib/errorHandler"
|
import AjaxErrorHandler from "@/lib/errorHandler"
|
||||||
|
import twemoji from "twemoji"
|
||||||
|
const emojis = require("../lib/emojis.json")
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "CommsInput",
|
name: "CommsInput",
|
||||||
|
@ -113,7 +286,16 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
uploadPercentage: 0,
|
||||||
|
uploading: false,
|
||||||
|
poll: {
|
||||||
|
dialog: false,
|
||||||
|
title: "",
|
||||||
|
options: ["", ""],
|
||||||
|
description: ""
|
||||||
|
},
|
||||||
message: "",
|
message: "",
|
||||||
|
embeds: [],
|
||||||
file: null,
|
file: null,
|
||||||
blobURL: null,
|
blobURL: null,
|
||||||
mentions: false,
|
mentions: false,
|
||||||
|
@ -122,7 +304,57 @@ export default {
|
||||||
completionWord: ""
|
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: {
|
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() {
|
tabCompletion() {
|
||||||
if (!this.completions.length) {
|
if (!this.completions.length) {
|
||||||
const word = this.message.split(" ").pop().toLowerCase()
|
const word = this.message.split(" ").pop().toLowerCase()
|
||||||
|
@ -260,7 +492,6 @@ export default {
|
||||||
let message = this.message
|
let message = this.message
|
||||||
this.message = ""
|
this.message = ""
|
||||||
if (this.file || message.length > 0) {
|
if (this.file || message.length > 0) {
|
||||||
const emojis = require("../lib/emojis.json")
|
|
||||||
message = message.replaceAll(
|
message = message.replaceAll(
|
||||||
/:([a-zA-Z0-9_\-+]+):/g,
|
/:([a-zA-Z0-9_\-+]+):/g,
|
||||||
(match, group1) => {
|
(match, group1) => {
|
||||||
|
@ -283,25 +514,36 @@ export default {
|
||||||
"/message",
|
"/message",
|
||||||
{
|
{
|
||||||
message: message,
|
message: message,
|
||||||
replyId: this.replying?.id
|
replyId: this.replying?.id,
|
||||||
|
embeds: this.embeds
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.focusInput()
|
this.focusInput()
|
||||||
this.autoScroll()
|
this.autoScroll()
|
||||||
this.endSend()
|
this.endSend()
|
||||||
|
this.embeds = []
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
AjaxErrorHandler(this.$store)(e)
|
AjaxErrorHandler(this.$store)(e)
|
||||||
})
|
})
|
||||||
} else {
|
} 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()
|
const formData = new FormData()
|
||||||
formData.append("message", message)
|
formData.append("message", message)
|
||||||
if (this.replying) {
|
if (this.replying) {
|
||||||
formData.append("replyId", this.replying.id)
|
formData.append("replyId", this.replying.id)
|
||||||
}
|
}
|
||||||
formData.append("file", this.file)
|
formData.append("file", this.file)
|
||||||
|
formData.append("embeds", this.embeds)
|
||||||
this.axios
|
this.axios
|
||||||
.post(
|
.post(
|
||||||
process.env.VUE_APP_BASE_URL +
|
process.env.VUE_APP_BASE_URL +
|
||||||
|
@ -312,7 +554,14 @@ export default {
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data"
|
"Content-Type": "multipart/form-data"
|
||||||
}
|
},
|
||||||
|
onUploadProgress: function (progressEvent) {
|
||||||
|
this.uploadPercentage = parseInt(
|
||||||
|
Math.round(
|
||||||
|
(progressEvent.loaded / progressEvent.total) * 100
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}.bind(this)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -320,8 +569,13 @@ export default {
|
||||||
this.autoScroll()
|
this.autoScroll()
|
||||||
this.endSend()
|
this.endSend()
|
||||||
this.file = null
|
this.file = null
|
||||||
|
this.uploading = false
|
||||||
|
this.uploadPercentage = 0
|
||||||
|
this.embeds = []
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
this.uploading = false
|
||||||
|
this.uploadPercentage = 0
|
||||||
console.log(e)
|
console.log(e)
|
||||||
AjaxErrorHandler(this.$store)(e)
|
AjaxErrorHandler(this.$store)(e)
|
||||||
})
|
})
|
||||||
|
|
|
@ -36,7 +36,6 @@
|
||||||
{{ message.reply.content.substring(0, 100) }}
|
{{ message.reply.content.substring(0, 100) }}
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
<v-list-item
|
<v-list-item
|
||||||
class="max-v-list-height"
|
|
||||||
:key="message.keyId"
|
:key="message.keyId"
|
||||||
:class="{
|
:class="{
|
||||||
'message-hover': hover,
|
'message-hover': hover,
|
||||||
|
@ -46,7 +45,9 @@
|
||||||
:dense="lastMessage"
|
:dense="lastMessage"
|
||||||
:id="'message-' + index"
|
:id="'message-' + index"
|
||||||
@contextmenu="show($event, 'message', message)"
|
@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-avatar size="45" class="mr-2" v-if="!lastMessage">
|
||||||
<v-img
|
<v-img
|
||||||
|
@ -130,6 +131,47 @@
|
||||||
no-gutters
|
no-gutters
|
||||||
>
|
>
|
||||||
<v-card
|
<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"
|
elevation="0"
|
||||||
:color="
|
:color="
|
||||||
embed.type === 'embed-v1' ? embed.backgroundColor : 'bg'
|
embed.type === 'embed-v1' ? embed.backgroundColor : 'bg'
|
||||||
|
@ -186,36 +228,6 @@
|
||||||
</p>
|
</p>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</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-row v-else-if="embed.type === 'embed-v1'">
|
||||||
<v-col
|
<v-col
|
||||||
cols="12"
|
cols="12"
|
||||||
|
@ -306,6 +318,59 @@
|
||||||
</v-container>
|
</v-container>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-row>
|
</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>
|
||||||
<template v-if="edit.id !== message.id">
|
<template v-if="edit.id !== message.id">
|
||||||
<v-card
|
<v-card
|
||||||
|
@ -330,7 +395,8 @@
|
||||||
<v-img
|
<v-img
|
||||||
@click="setImagePreview(attachment)"
|
@click="setImagePreview(attachment)"
|
||||||
contain
|
contain
|
||||||
:aspect-ratio="16 / 9"
|
:max-width="500"
|
||||||
|
:max-height="500"
|
||||||
:src="
|
:src="
|
||||||
$store.state.baseURL +
|
$store.state.baseURL +
|
||||||
'/usercontent/' +
|
'/usercontent/' +
|
||||||
|
@ -345,6 +411,8 @@
|
||||||
>
|
>
|
||||||
<v-progress-circular
|
<v-progress-circular
|
||||||
indeterminate
|
indeterminate
|
||||||
|
width="500"
|
||||||
|
height="500"
|
||||||
color="grey lighten-5"
|
color="grey lighten-5"
|
||||||
></v-progress-circular>
|
></v-progress-circular>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
@ -899,6 +967,24 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
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() {
|
mentioned() {
|
||||||
return this.message.content
|
return this.message.content
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
@ -906,6 +992,15 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
votePoll(option) {
|
||||||
|
this.axios
|
||||||
|
.post(`/api/v1/polls/${this.message.poll.id}/vote`, {
|
||||||
|
option
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
AjaxErrorHandler(this.$store)(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
pinMessage() {
|
pinMessage() {
|
||||||
this.axios
|
this.axios
|
||||||
.post(`/api/v1/communications/${this.chat.id}/pins`, {
|
.post(`/api/v1/communications/${this.chat.id}/pins`, {
|
||||||
|
@ -929,6 +1024,18 @@ export default {
|
||||||
return (size / 1073741824).toFixed(2) + " GB"
|
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>
|
</script>
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -74,9 +74,16 @@ export default new Vuex.Store({
|
||||||
wsRegistered: false,
|
wsRegistered: false,
|
||||||
lastChat: "friends",
|
lastChat: "friends",
|
||||||
searchPanel: false,
|
searchPanel: false,
|
||||||
userPanel: false
|
userPanel: false,
|
||||||
|
messages: {}
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
|
setMessages(state, { id, messages }) {
|
||||||
|
state.messages[id] = messages
|
||||||
|
},
|
||||||
|
appendMessage(state, { id, message }) {
|
||||||
|
state.messages[id].push(message)
|
||||||
|
},
|
||||||
setSelectedChat(state, chat) {
|
setSelectedChat(state, chat) {
|
||||||
state.selectedChat = 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: {
|
watch: {
|
||||||
selectedChat() {
|
selectedChat() {
|
||||||
this.$store.commit("setSelectedChat", this.selectedChat)
|
this.$store.commit("setSelectedChat", this.selectedChat)
|
||||||
|
|
|
@ -97,7 +97,6 @@
|
||||||
:max-width="1000"
|
:max-width="1000"
|
||||||
:max-height="600"
|
:max-height="600"
|
||||||
:min-height="300"
|
:min-height="300"
|
||||||
aspect-ratio="16/9"
|
|
||||||
contain
|
contain
|
||||||
></v-img>
|
></v-img>
|
||||||
<v-container>
|
<v-container>
|
||||||
|
@ -1065,7 +1064,11 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async getMessages() {
|
async getMessages() {
|
||||||
this.loadingMessages = true
|
if (!this.$store.state.messages[this.chat.id]) {
|
||||||
|
this.loadingMessages = true
|
||||||
|
} else {
|
||||||
|
this.autoScroll()
|
||||||
|
}
|
||||||
await this.axios
|
await this.axios
|
||||||
.get(
|
.get(
|
||||||
process.env.VUE_APP_BASE_URL +
|
process.env.VUE_APP_BASE_URL +
|
||||||
|
@ -1079,6 +1082,10 @@ export default {
|
||||||
this.reachedTop = true
|
this.reachedTop = true
|
||||||
}
|
}
|
||||||
this.messages.unshift(...res.data)
|
this.messages.unshift(...res.data)
|
||||||
|
/* this.$store.commit("setMessages", {
|
||||||
|
id: this.chat.id,
|
||||||
|
messages: this.messages
|
||||||
|
})*/
|
||||||
this.loadingMessages = false
|
this.loadingMessages = false
|
||||||
this.markRead()
|
this.markRead()
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
@ -1229,7 +1236,7 @@ export default {
|
||||||
drafts[oldVal] = ""
|
drafts[oldVal] = ""
|
||||||
}
|
}
|
||||||
this.message = drafts[val] || ""
|
this.message = drafts[val] || ""
|
||||||
this.messages = []
|
this.messages = this.$store.state.messages[val] || []
|
||||||
this.usersTyping = []
|
this.usersTyping = []
|
||||||
this.replying = null
|
this.replying = null
|
||||||
this.reachedTop = false
|
this.reachedTop = false
|
||||||
|
|
|
@ -88,7 +88,7 @@
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
<v-card color="card" elevation="0">
|
<v-card color="card" elevation="0">
|
||||||
<v-list color="card">
|
<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-content>
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
You currently do not have any incoming friend requests.
|
You currently do not have any incoming friend requests.
|
||||||
|
@ -141,7 +141,7 @@
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
<v-card color="card" elevation="0">
|
<v-card color="card" elevation="0">
|
||||||
<v-list color="card">
|
<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-content>
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
You currently do not have any outgoing friend requests.
|
You currently do not have any outgoing friend requests.
|
||||||
|
@ -191,7 +191,7 @@
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
<v-card color="card" elevation="0">
|
<v-card color="card" elevation="0">
|
||||||
<v-list color="card">
|
<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-content>
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
You currently do not have any friends.
|
You currently do not have any friends.
|
||||||
|
@ -374,7 +374,6 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.$store.dispatch("doInit")
|
|
||||||
this.getFriends()
|
this.getFriends()
|
||||||
this.getUsers()
|
this.getUsers()
|
||||||
this.$socket.on("friendRequest", () => {
|
this.$socket.on("friendRequest", () => {
|
||||||
|
|
Loading…
Reference in a new issue