Compare commits
20 Commits
843659b495
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adb7a1161a | ||
|
|
3c069ed2f8 | ||
|
|
ae73326820 | ||
|
|
f40b6758de | ||
|
|
43e8cc7d3e | ||
|
|
edddf9afbf | ||
|
|
3bffd88108 | ||
|
|
0094783a4e | ||
|
|
f42d819e45 | ||
|
|
467b0eebe9 | ||
|
|
e8e8555c2f | ||
|
|
af4cc2f6a0 | ||
|
|
2cae8f12a7 | ||
|
|
7be2e84691 | ||
|
|
737a22e1f8 | ||
|
|
4ecd723cec | ||
|
|
53c79aebc4 | ||
|
|
b2b0949353 | ||
|
|
ffcfc57bbe | ||
|
|
f7c95b3a36 |
33
README.md
33
README.md
@@ -1,28 +1,31 @@
|
|||||||
# TRIFORM
|
# TRIFORM
|
||||||
*WORK IN PROGRESS*
|
|
||||||
|
_WORK IN PROGRESS_
|
||||||
|
|
||||||
TRIFORM (Threaded Rooms Interface For Organised Relational Mails) is a mail client which sorts mails into conversations based on the members present in each mails.
|
TRIFORM (Threaded Rooms Interface For Organised Relational Mails) is a mail client which sorts mails into conversations based on the members present in each mails.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- [x] Multi account
|
- [x] Multi account (tested only with gmail)
|
||||||
- [x] Live syncing of mails
|
- [x] Live syncing of mails
|
||||||
- [x] Live syncing of flags
|
- [x] Live syncing of flags
|
||||||
- [x] Mark as read/unread
|
- [x] Mark as read/unread
|
||||||
- [ ] Attachments
|
- [x] Flag important
|
||||||
- [ ] Send new mails
|
- [ ] Attachments
|
||||||
- [ ] Respond to mails
|
- [ ] Send new mails
|
||||||
- [ ] Delete mails
|
- [ ] Respond to mails
|
||||||
- [ ] Live sync with the ui
|
- [x] Delete mails
|
||||||
|
- [ ] Delete rooms
|
||||||
|
- [ ] Live sync with the ui
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
## Definitions
|
## Definitions
|
||||||
|
|
||||||
- Room: General space where messages are grouped.
|
- Room: General space where messages are grouped.
|
||||||
- Channel: Space use for newsletters or other conversations when there is a low need for reply.
|
- Channel: Space use for newsletters or other conversations when there is a low need for reply.
|
||||||
- Group: Space contening several users that are part of the conversation.
|
- Group: Space contening several users that are part of the conversation.
|
||||||
- DM: Space defining a conversation with only one other member.
|
- DM: Space defining a conversation with only one other member.
|
||||||
- Thread: Sub-Space generally created when changing the number of member in a space.
|
- Thread: Sub-Space generally created when changing the number of member in a space.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ export default class Account {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async register(body, res: Response) {
|
static async register(body, res: Response) {
|
||||||
const { email, pwd, xoauth, xoauth2, host, port, tls } = body;
|
const { email, pwd, xoauth, xoauth2, imapHost, smtpHost, imapPort, smtpPort, tls } = body;
|
||||||
getAddressId(email).then((addressId) => {
|
getAddressId(email).then((addressId) => {
|
||||||
registerAccount(addressId, pwd, xoauth, xoauth2, host, port, tls)
|
registerAccount(addressId, pwd, xoauth, xoauth2, imapHost, smtpHost, imapPort, smtpPort, tls)
|
||||||
.then((mailboxId) => {
|
.then((mailboxId) => {0
|
||||||
res.status(statusCodes.OK).json({ id: mailboxId });
|
res.status(statusCodes.OK).json({ id: mailboxId });
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -1,133 +1,120 @@
|
|||||||
import statusCode from "../utils/statusCodes";
|
import statusCode from "../utils/statusCodes";
|
||||||
import { Response } from "express";
|
import { Response } from "express";
|
||||||
import { getMessageUid, getUserOfMailbox } from "../db/utils/mail";
|
|
||||||
import emailManager from "../mails/EmailManager";
|
|
||||||
import { deleteMessage } from "../db/message/updateMessage-db";
|
|
||||||
import logger from "../system/Logger";
|
import logger from "../system/Logger";
|
||||||
import { deleteRoom, getRoomNbMessage, getRoomOnMessageId } from "../db/Room-db";
|
import Message from "../mails/message/Message";
|
||||||
|
import Room from "../mails/room/Room";
|
||||||
|
|
||||||
export default class Message {
|
export default class MessageAbl {
|
||||||
static async addFlag(body, res: Response) {
|
static async changeFlag(body, res: Response, isDelete: boolean) {
|
||||||
const { mailboxId, messageId, flag } = body;
|
const { mailboxId, messageId, flag } = body;
|
||||||
const uid = (await getMessageUid(messageId))[0]?.uid;
|
const message = new Message().setMessageId(messageId);
|
||||||
if (!uid) {
|
|
||||||
|
try {
|
||||||
|
await message.useUid();
|
||||||
|
} catch (err) {
|
||||||
res.status(statusCode.NOT_FOUND).send({ error: "Message uid not found." });
|
res.status(statusCode.NOT_FOUND).send({ error: "Message uid not found." });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = (await getUserOfMailbox(mailboxId))[0]?.user;
|
try {
|
||||||
if (!user) {
|
await message.useMailbox(mailboxId);
|
||||||
|
} catch (err) {
|
||||||
res.status(statusCode.NOT_FOUND).send({ error: "Not account for this mailbox." });
|
res.status(statusCode.NOT_FOUND).send({ error: "Not account for this mailbox." });
|
||||||
}
|
}
|
||||||
emailManager
|
|
||||||
.getImap(user)
|
try {
|
||||||
.getMailbox(mailboxId)
|
if (isDelete) {
|
||||||
.addFlag(uid.toString(), [flag])
|
await message.mailbox.removeFlag(message.uid.toString(), flag);
|
||||||
.then(() => {
|
} else {
|
||||||
res.status(statusCode.OK).send();
|
await message.mailbox.addFlag(message.uid.toString(), flag);
|
||||||
})
|
}
|
||||||
.catch((err) => {
|
} catch (err) {
|
||||||
console.log(err);
|
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
||||||
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
}
|
||||||
});
|
res.status(statusCode.OK).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
static async addFlag(body, res: Response) {
|
||||||
|
await MessageAbl.changeFlag(body, res, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async removeFlag(body, res: Response) {
|
static async removeFlag(body, res: Response) {
|
||||||
const { mailboxId, messageId, flag } = body;
|
await MessageAbl.changeFlag(body, res, true);
|
||||||
const uid = (await getMessageUid(messageId))[0]?.uid;
|
}
|
||||||
if (!uid) {
|
|
||||||
|
static async deleteRemoteUtil(message: Message, mailboxId: number, res, isFull: boolean): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await message.useUid();
|
||||||
|
} catch (error) {
|
||||||
|
logger.err(error);
|
||||||
res.status(statusCode.NOT_FOUND).send({ error: "Message uid not found." });
|
res.status(statusCode.NOT_FOUND).send({ error: "Message uid not found." });
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = (await getUserOfMailbox(mailboxId))[0]?.user;
|
try {
|
||||||
if (!user) {
|
await message.useMailbox(mailboxId);
|
||||||
|
} catch (error) {
|
||||||
res.status(statusCode.NOT_FOUND).send({ error: "Not account for this mailbox." });
|
res.status(statusCode.NOT_FOUND).send({ error: "Not account for this mailbox." });
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
emailManager
|
|
||||||
.getImap(user)
|
try {
|
||||||
.getMailbox(mailboxId)
|
await message.mailbox.addFlag(message.uid.toString(), ["\\Deleted"]);
|
||||||
.removeFlag(uid.toString(), [flag])
|
await message.mailbox.moveToTrash(message.uid.toString(), (err) => {
|
||||||
.then(() => {
|
if (err) {
|
||||||
res.status(statusCode.OK).send();
|
logger.err(err);
|
||||||
})
|
}
|
||||||
.catch((err) => {
|
// throw err;
|
||||||
console.log(err);
|
|
||||||
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.log(err);
|
||||||
|
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFull) {
|
||||||
|
res.status(statusCode.OK).send();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static deleteRemoteOnly = async (body, res: Response) => {
|
static deleteRemoteOnly = async (body, res: Response) => {
|
||||||
const { mailboxId, messageId } = body;
|
const { mailboxId, messageId } = body;
|
||||||
const uid = (await getMessageUid(messageId))[0]?.uid;
|
const message = new Message().setMessageId(messageId);
|
||||||
if (!uid) {
|
await MessageAbl.deleteRemoteUtil(message, mailboxId, res, true);
|
||||||
res.status(statusCode.NOT_FOUND).send({ error: "Message uid not found." });
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = (await getUserOfMailbox(mailboxId))[0]?.user;
|
|
||||||
if (!user) {
|
|
||||||
res.status(statusCode.NOT_FOUND).send({ error: "Not account for this mailbox." });
|
|
||||||
}
|
|
||||||
const mailbox = emailManager.getImap(user).getMailbox(mailboxId);
|
|
||||||
|
|
||||||
// add flag for deletion
|
|
||||||
mailbox
|
|
||||||
.addFlag(uid.toString(), ["\\Deleted"])
|
|
||||||
.then(() => {
|
|
||||||
// move message to trash
|
|
||||||
mailbox.moveToTrash(uid.toString(), (err) => {
|
|
||||||
if (err) {
|
|
||||||
logger.err(err);
|
|
||||||
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
|
||||||
} else {
|
|
||||||
res.status(statusCode.OK).send();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.log(err);
|
|
||||||
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static async deleteEverywhere(body, res: Response) {
|
static deleteEverywhere = async (body, res: Response) => {
|
||||||
const { mailboxId, messageId } = body;
|
const { mailboxId, messageId } = body;
|
||||||
const uid = (await getMessageUid(messageId))[0]?.uid;
|
const message = new Message().setMessageId(messageId);
|
||||||
if (!uid) {
|
|
||||||
res.status(statusCode.NOT_FOUND).send({ error: "Message uid not found." });
|
try {
|
||||||
|
await message.useFlags();
|
||||||
|
} catch (err) {
|
||||||
|
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
||||||
|
logger.err(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = (await getUserOfMailbox(mailboxId))[0]?.user;
|
// if message not deleted remotly, delete it
|
||||||
if (!user) {
|
if (!message.isDeleted) {
|
||||||
res.status(statusCode.NOT_FOUND).send({ error: "Not account for this mailbox." });
|
const success = await MessageAbl.deleteRemoteUtil(message, mailboxId, res, false);
|
||||||
return;
|
if (!success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const roomId = (await getRoomOnMessageId(messageId))[0]?.room_id;
|
const room = await new Room().setRoomIdOnMessageId(messageId);
|
||||||
|
try {
|
||||||
emailManager
|
await message.delete();
|
||||||
.getImap(user)
|
if (room.roomId && await room.shouldDelete()) {
|
||||||
.getMailbox(mailboxId)
|
await room.delete();
|
||||||
.removeFlag(uid.toString(), ["\\Deleted"])
|
res.status(statusCode.OK).json({ deleteRoom: true }).send();
|
||||||
.then(async () => {
|
return;
|
||||||
try {
|
}
|
||||||
await deleteMessage(messageId);
|
} catch (err) {
|
||||||
if (roomId) {
|
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
||||||
const nbMessage = (await getRoomNbMessage(roomId))[0].nbMessage;
|
return;
|
||||||
if (nbMessage > 0) {
|
}
|
||||||
res.status(statusCode.OK).send();
|
res.status(statusCode.OK).send();
|
||||||
} else {
|
};
|
||||||
await deleteRoom(roomId);
|
|
||||||
res.status(statusCode.OK).json({ deleteRoom: true }).send();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
res.status(statusCode.METHOD_FAILURE).send({ error: err });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import MailBuilder from "../mails/utils/mailBuilder";
|
|||||||
import { getAddresses } from "../db/utils/mail";
|
import { getAddresses } from "../db/utils/mail";
|
||||||
import { getMembers, getMessages, getRooms } from "../db/api-db";
|
import { getMembers, getMessages, getRooms } from "../db/api-db";
|
||||||
import logger from "../system/Logger";
|
import logger from "../system/Logger";
|
||||||
|
import Room from "../mails/room/Room";
|
||||||
|
|
||||||
function rmUserFromAddrs(addresses: { email: string }[], user: string) {
|
function rmUserFromAddrs(addresses: { email: string }[], user: string) {
|
||||||
let index = addresses.findIndex((a) => a.email == user);
|
let index = addresses.findIndex((a) => a.email == user);
|
||||||
@@ -15,7 +16,7 @@ function rmUserFromAddrs(addresses: { email: string }[], user: string) {
|
|||||||
addresses.splice(index, 1);
|
addresses.splice(index, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default class Room {
|
export default class RoomAbl {
|
||||||
// todo change name of reponse
|
// todo change name of reponse
|
||||||
static async response(body, res: Response) {
|
static async response(body, res: Response) {
|
||||||
const { user, roomId, text, html } = body;
|
const { user, roomId, text, html } = body;
|
||||||
@@ -27,7 +28,7 @@ export default class Room {
|
|||||||
const mailBuilder = new MailBuilder();
|
const mailBuilder = new MailBuilder();
|
||||||
mailBuilder.from(user).to(ownerEmail).text(text).html(html);
|
mailBuilder.from(user).to(ownerEmail).text(text).html(html);
|
||||||
|
|
||||||
emailManager.getSmtp(user).sendMail(mailBuilder.message);
|
emailManager.getSmtp(user)?.sendMail(mailBuilder.message);
|
||||||
res.status(statusCode.OK).send();
|
res.status(statusCode.OK).send();
|
||||||
} else if (roomType === RoomType.GROUP || roomType === RoomType.THREAD) {
|
} else if (roomType === RoomType.GROUP || roomType === RoomType.THREAD) {
|
||||||
const lastMsgData = (await getLastMsgData(roomId))[0];
|
const lastMsgData = (await getLastMsgData(roomId))[0];
|
||||||
@@ -52,7 +53,7 @@ export default class Room {
|
|||||||
.to(to.map((a) => a.email))
|
.to(to.map((a) => a.email))
|
||||||
.cc(cc.map((a) => a.email));
|
.cc(cc.map((a) => a.email));
|
||||||
|
|
||||||
emailManager.getSmtp(user).sendMail(mailBuilder.message);
|
emailManager.getSmtp(user)?.sendMail(mailBuilder.message);
|
||||||
res.status(statusCode.OK).send();
|
res.status(statusCode.OK).send();
|
||||||
} else {
|
} else {
|
||||||
res.status(statusCode.FORBIDDEN).send({ error: "Cannot add a new message in a room or a channel." });
|
res.status(statusCode.FORBIDDEN).send({ error: "Cannot add a new message in a room or a channel." });
|
||||||
@@ -71,14 +72,22 @@ export default class Room {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getMembers(body, res) {
|
static async getMembers(body, res: Response) {
|
||||||
const { roomId } = body;
|
const { roomId } = body;
|
||||||
getMembers(roomId).then((addresses) => {
|
getMembers(roomId)
|
||||||
res.status(statusCode.OK).json(addresses);
|
.then((addresses) => {
|
||||||
}).catch((err) => {
|
res.status(statusCode.OK).json(addresses);
|
||||||
logger.err(err)
|
})
|
||||||
res.status(statusCode.INTERNAL_SERVER_ERROR);
|
.catch((err) => {
|
||||||
});
|
logger.err(err);
|
||||||
|
res.status(statusCode.INTERNAL_SERVER_ERROR);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async delete(body, res: Response) {
|
||||||
|
const { roomId } = body;
|
||||||
|
console.log("delete", roomId);
|
||||||
|
const room = new Room().setRoomId(roomId);
|
||||||
|
// todo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,8 +68,12 @@ export async function getRoomOnMessageId(messageId: number) {
|
|||||||
return await execQueryAsync(query, values);
|
return await execQueryAsync(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRoomNbMessage(roomId: number) {
|
export async function getRoomNbMessageAndThread(roomId: number): Promise<{ nbMessage: number; nbThread: number }[]> {
|
||||||
const query = `SELECT COUNT(room_id) AS nbMessage FROM app_room_message WHERE room_id = ?`;
|
const query = `
|
||||||
|
SELECT COUNT(arm.room_id) AS nbMessage, COUNT(app_thread.room_id) AS nbThread
|
||||||
|
FROM app_room_message arm
|
||||||
|
INNER JOIN app_thread ON (app_thread.root_id = arm.room_id OR app_thread.parent_id = arm.room_id)
|
||||||
|
WHERE arm.room_id = ?`;
|
||||||
const values = [roomId];
|
const values = [roomId];
|
||||||
return await execQueryAsync(query, values);
|
return await execQueryAsync(query, values);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { execQueryAsync, execQueryAsyncWithId } from "./db";
|
import { execQueryAsync, execQueryAsyncWithId } from "./db";
|
||||||
import { queryCcId, queryToId, queryFromId } from "./utils/addressQueries";
|
import { queryCcId, queryToId, queryFromId } from "./utils/addressQueries";
|
||||||
|
|
||||||
export async function registerAccount(userId, pwd, xoauth, xoauth2, host, port, tls) {
|
export async function registerAccount(userId, pwd, xoauth, xoauth2, imapHost, smtpHost, imapPort, smtpPort, tls) {
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO app_account
|
INSERT INTO app_account
|
||||||
(user_id, account_pwd, xoauth, xoauth2, host, port, tls) VALUES (?, ?, ?, ?, ?, ?, ?)
|
(user_id, account_pwd, xoauth, xoauth2, imap_host, smtp_host, imap_port, smtp_port, tls) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`;
|
`;
|
||||||
const values = [userId, pwd, xoauth, xoauth2, host, port, tls];
|
const values = [userId, pwd, xoauth, xoauth2, imapHost, smtpHost, imapPort, smtpPort, tls];
|
||||||
return await execQueryAsyncWithId(query, values);
|
return await execQueryAsyncWithId(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@ export async function getRooms(mailboxId: number) {
|
|||||||
room.room_type AS roomType,
|
room.room_type AS roomType,
|
||||||
mailbox_message.mailbox_id AS mailboxId,
|
mailbox_message.mailbox_id AS mailboxId,
|
||||||
app_thread.parent_id
|
app_thread.parent_id
|
||||||
|
|
||||||
FROM app_room room
|
FROM app_room room
|
||||||
INNER JOIN message ON message.message_id = room.message_id
|
INNER JOIN message ON message.message_id = room.message_id
|
||||||
INNER JOIN mailbox_message ON mailbox_message.message_id = message.message_id
|
INNER JOIN mailbox_message ON mailbox_message.message_id = message.message_id
|
||||||
@@ -55,6 +56,7 @@ export async function getRooms(mailboxId: number) {
|
|||||||
)
|
)
|
||||||
) notSeenRoom ON notSeenRoom.room_id = room.room_id
|
) notSeenRoom ON notSeenRoom.room_id = room.room_id
|
||||||
|
|
||||||
|
-- get not seen in thread
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT app_room_message.message_id, app_thread.parent_id
|
SELECT app_room_message.message_id, app_thread.parent_id
|
||||||
FROM app_room
|
FROM app_room
|
||||||
@@ -89,7 +91,8 @@ export async function getMessages(roomId: number) {
|
|||||||
subjectT.value AS subject,
|
subjectT.value AS subject,
|
||||||
content.text AS content,
|
content.text AS content,
|
||||||
message.idate AS date,
|
message.idate AS date,
|
||||||
GROUP_CONCAT(flagT.flag_name) AS flags
|
GROUP_CONCAT(flagT.flag_name) AS flags,
|
||||||
|
thread.room_id AS thread
|
||||||
FROM app_room_message msg
|
FROM app_room_message msg
|
||||||
|
|
||||||
${queryFromId} fromT ON msg.message_id = fromT.message_id
|
${queryFromId} fromT ON msg.message_id = fromT.message_id
|
||||||
@@ -106,7 +109,8 @@ export async function getMessages(roomId: number) {
|
|||||||
) subjectT ON msg.message_id = subjectT.message_id
|
) subjectT ON msg.message_id = subjectT.message_id
|
||||||
|
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT bodypart.text, header_field.message_id FROM bodypart
|
SELECT bodypart.text, header_field.message_id
|
||||||
|
FROM bodypart
|
||||||
INNER JOIN header_field
|
INNER JOIN header_field
|
||||||
INNER JOIN field_name
|
INNER JOIN field_name
|
||||||
WHERE
|
WHERE
|
||||||
@@ -120,6 +124,13 @@ export async function getMessages(roomId: number) {
|
|||||||
|
|
||||||
INNER JOIN message ON message.message_id = msg.message_id
|
INNER JOIN message ON message.message_id = msg.message_id
|
||||||
|
|
||||||
|
-- get room_id of thread room with this message as origin
|
||||||
|
LEFT JOIN app_room thread ON (
|
||||||
|
thread.message_id = msg.message_id AND
|
||||||
|
msg.room_id != thread.room_id AND
|
||||||
|
thread.room_type = 4
|
||||||
|
)
|
||||||
|
|
||||||
WHERE msg.room_id = ?
|
WHERE msg.room_id = ?
|
||||||
GROUP BY msg.message_id
|
GROUP BY msg.message_id
|
||||||
ORDER BY message.idate ASC;
|
ORDER BY message.idate ASC;
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ CREATE TABLE app_account (
|
|||||||
account_pwd BINARY(22),
|
account_pwd BINARY(22),
|
||||||
xoauth VARCHAR(116),
|
xoauth VARCHAR(116),
|
||||||
xoauth2 VARCHAR(116),
|
xoauth2 VARCHAR(116),
|
||||||
host VARCHAR(255) NOT NULL DEFAULT 'localhost',
|
imap_host VARCHAR(255) NOT NULL DEFAULT 'localhost',
|
||||||
port INT(5) NOT NULL DEFAULT 143,
|
imap_port INT(5) NOT NULL DEFAULT 993,
|
||||||
|
smtp_host VARCHAR(255) NOT NULL DEFAULT 'localhost',
|
||||||
|
smtp_port INT(5) NOT NULL DEFAULT 465,
|
||||||
tls BOOLEAN NOT NULL DEFAULT true,
|
tls BOOLEAN NOT NULL DEFAULT true,
|
||||||
PRIMARY KEY (account_id),
|
PRIMARY KEY (account_id),
|
||||||
FOREIGN KEY (user_id) REFERENCES address(address_id) ON DELETE CASCADE
|
FOREIGN KEY (user_id) REFERENCES address(address_id) ON DELETE CASCADE
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ export async function getAllAccounts() {
|
|||||||
app_account.account_id AS id,
|
app_account.account_id AS id,
|
||||||
address.email AS user,
|
address.email AS user,
|
||||||
app_account.account_pwd AS password,
|
app_account.account_pwd AS password,
|
||||||
app_account.host AS host,
|
app_account.imap_host,
|
||||||
app_account.port AS port,
|
app_account.imap_port,
|
||||||
app_account.tls AS tls
|
app_account.smtp_host,
|
||||||
|
app_account.smtp_port,
|
||||||
|
app_account.tls
|
||||||
FROM app_account INNER JOIN address
|
FROM app_account INNER JOIN address
|
||||||
WHERE address.address_id = app_account.user_id
|
WHERE address.address_id = app_account.user_id
|
||||||
`;
|
`;
|
||||||
|
|||||||
23
back/db/message/message-db.ts
Normal file
23
back/db/message/message-db.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { execQueryAsync } from "../db";
|
||||||
|
|
||||||
|
export async function getFlagsOnUid(uid: number): Promise<{ flag_id: number; flag_name: string }[]> {
|
||||||
|
const query = `
|
||||||
|
SELECT flag_name FROM flag_name
|
||||||
|
INNER JOIN flag ON flag.flag_id = flag_name.flag_id
|
||||||
|
INNER JOIN mailbox_message ON mailbox_message.message_id = flag.message_id
|
||||||
|
WHERE mailbox_message.uid = ?
|
||||||
|
`;
|
||||||
|
const values = [uid];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFlagsOnId(messageId: number): Promise<{ flag_id: number; flag_name: string }[]> {
|
||||||
|
const query = `
|
||||||
|
SELECT flag_name FROM flag_name
|
||||||
|
INNER JOIN flag ON flag.flag_id = flag_name.flag_id
|
||||||
|
INNER JOIN mailbox_message ON mailbox_message.message_id = flag.message_id
|
||||||
|
WHERE mailbox_message.message_id = ?
|
||||||
|
`;
|
||||||
|
const values = [messageId];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
@@ -19,13 +19,13 @@ export async function deleteFlag(messageId: number, flagId: number) {
|
|||||||
|
|
||||||
export async function updateMailboxSeen(messageId: number, isSeen: boolean) {
|
export async function updateMailboxSeen(messageId: number, isSeen: boolean) {
|
||||||
const query = `UPDATE mailbox_message SET seen = ? WHERE message_id = ?`;
|
const query = `UPDATE mailbox_message SET seen = ? WHERE message_id = ?`;
|
||||||
const values = [messageId, isSeen];
|
const values = [isSeen, messageId];
|
||||||
return await execQueryAsync(query, values);
|
return await execQueryAsync(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateMailboxDeleted(messageId: number, isDeleted: boolean) {
|
export async function updateMailboxDeleted(messageId: number, isDeleted: boolean) {
|
||||||
const query = `UPDATE mailbox_message SET deleted = ? WHERE message_id = ?`;
|
const query = `UPDATE mailbox_message SET deleted = ? WHERE message_id = ?`;
|
||||||
const values = [messageId, isDeleted];
|
const values = [isDeleted, messageId];
|
||||||
return await execQueryAsync(query, values);
|
return await execQueryAsync(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ export async function getMessageUid(messageId: number): Promise<{uid: number}[]>
|
|||||||
return await execQueryAsync(query, values);
|
return await execQueryAsync(query, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getMessageIdOnID(messageID: string): Promise<{message_id: number}[]> {
|
||||||
|
const query = `SELECT message_id FROM message WHERE messageID = ?`;
|
||||||
|
const values = [messageID];
|
||||||
|
return await execQueryAsync(query, values);
|
||||||
|
}
|
||||||
|
|
||||||
export async function findRoomByOwner(ownerId: number): Promise<{ room_id: number }[]> {
|
export async function findRoomByOwner(ownerId: number): Promise<{ room_id: number }[]> {
|
||||||
const query = `SELECT room_id FROM app_room WHERE owner_id = ?`;
|
const query = `SELECT room_id FROM app_room WHERE owner_id = ?`;
|
||||||
const values = [ownerId];
|
const values = [ownerId];
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import Imap from "imap";
|
|||||||
import { getAllMailboxes, registerMailbox } from "../../db/imap/imap-db";
|
import { getAllMailboxes, registerMailbox } from "../../db/imap/imap-db";
|
||||||
import logger from "../../system/Logger";
|
import logger from "../../system/Logger";
|
||||||
import Mailbox from "./Mailbox";
|
import Mailbox from "./Mailbox";
|
||||||
import { rejects } from "assert";
|
|
||||||
|
|
||||||
export class ImapInstance {
|
export class ImapInstance {
|
||||||
imap: Imap;
|
imap: Imap;
|
||||||
@@ -15,9 +14,9 @@ export class ImapInstance {
|
|||||||
this.imap = new Imap({
|
this.imap = new Imap({
|
||||||
user: account.user,
|
user: account.user,
|
||||||
password: account.password,
|
password: account.password,
|
||||||
tlsOptions: { servername: account.host },
|
tlsOptions: { servername: account.imap_host },
|
||||||
host: account.host,
|
host: account.imap_host,
|
||||||
port: account.port,
|
port: account.imap_port,
|
||||||
tls: account.tls,
|
tls: account.tls,
|
||||||
});
|
});
|
||||||
this.account = account;
|
this.account = account;
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export default class Mailbox {
|
|||||||
});
|
});
|
||||||
|
|
||||||
f.once("error", (err) => {
|
f.once("error", (err) => {
|
||||||
logger.err("Fetch error: " + err);
|
logger.err("Fetch error when fetching in uid range: " + err);
|
||||||
reject(1);
|
reject(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
86
back/mails/message/Message.ts
Normal file
86
back/mails/message/Message.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { getFlagsOnId, getFlagsOnUid } from "../../db/message/message-db";
|
||||||
|
import { deleteMessage } from "../../db/message/updateMessage-db";
|
||||||
|
import { getMessageUid, getUserOfMailbox } from "../../db/utils/mail";
|
||||||
|
import emailManager from "../EmailManager";
|
||||||
|
import Mailbox from "../imap/Mailbox";
|
||||||
|
import Room from "../room/Room";
|
||||||
|
|
||||||
|
export default class Message {
|
||||||
|
messageId: number;
|
||||||
|
uid: number;
|
||||||
|
private _flags: string[] | undefined;
|
||||||
|
private _mailbox: Mailbox;
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
setMessageId(messageId: number): Message {
|
||||||
|
this.messageId = messageId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async useUid(): Promise<Message> {
|
||||||
|
if (!this.messageId) {
|
||||||
|
throw "Define message id before trying to find uid";
|
||||||
|
}
|
||||||
|
this.uid = (await getMessageUid(this.messageId))[0]?.uid;
|
||||||
|
if (!this.uid) {
|
||||||
|
throw "Uid not found";
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async useFlags(): Promise<Message> {
|
||||||
|
let flags;
|
||||||
|
if (!this._flags) {
|
||||||
|
if (this.messageId) {
|
||||||
|
flags = await getFlagsOnId(this.messageId);
|
||||||
|
} else if (this.uid) {
|
||||||
|
flags = await getFlagsOnUid(this.uid);
|
||||||
|
} else {
|
||||||
|
throw "Neither message id or uid are set, please do so before attempting to load flags";
|
||||||
|
}
|
||||||
|
this._flags = flags;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async useMailbox(mailboxId: number, user?: string): Promise<Message> {
|
||||||
|
if (!user) {
|
||||||
|
user = (await getUserOfMailbox(mailboxId))[0]?.user;
|
||||||
|
if (!user) {
|
||||||
|
throw "Cannot find user of this mailbox";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._mailbox = emailManager.getImap(user).getMailbox(mailboxId);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get mailbox() {
|
||||||
|
if (!this._mailbox) {
|
||||||
|
throw "Call useMailbox before calling functions related to mailbox";
|
||||||
|
}
|
||||||
|
return this._mailbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
get flags(): string[] {
|
||||||
|
console.log("called");
|
||||||
|
console.log(this._flags);
|
||||||
|
if (!this._flags) {
|
||||||
|
throw "Flags not loaded, call useFlags before calling functions related to flags";
|
||||||
|
} else {
|
||||||
|
return this._flags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isDeleted(): boolean {
|
||||||
|
return this.flags.includes("\\Deleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
delete = async (messageId?: number): Promise<Message> => {
|
||||||
|
if (!(this.messageId ?? messageId)) {
|
||||||
|
throw "Delete need to have the message id";
|
||||||
|
}
|
||||||
|
await deleteMessage(this.messageId ?? messageId);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
getThreadInfoOnId,
|
getThreadInfoOnId,
|
||||||
} from "../../db/message/saveMessage-db";
|
} from "../../db/message/saveMessage-db";
|
||||||
|
|
||||||
import { findRoomByOwner, getAddressId, getUserIdOfMailbox } from "../../db/utils/mail";
|
import { findRoomByOwner, getAddressId, getMessageIdOnID, getUserIdOfMailbox } from "../../db/utils/mail";
|
||||||
import { nbMembers } from "../utils/envelopeUtils";
|
import { nbMembers } from "../utils/envelopeUtils";
|
||||||
import logger from "../../system/Logger";
|
import logger from "../../system/Logger";
|
||||||
import { Attrs, Envelope, User } from "../../interfaces/mail/attrs.interface";
|
import { Attrs, Envelope, User } from "../../interfaces/mail/attrs.interface";
|
||||||
@@ -55,93 +55,6 @@ export default class RegisterMessageInApp {
|
|||||||
this.inReplyTo = "";
|
this.inReplyTo = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
|
||||||
if (this.envelope.from) {
|
|
||||||
this.ownerId = await getAddressId(createAddress(this.envelope.from[0])); // todo use sender or from ?
|
|
||||||
} else {
|
|
||||||
throw new Error("Envelope must have a 'from' field");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isDm = () => nbMembers(this.envelope) == 2;
|
|
||||||
|
|
||||||
async isFromUs() {
|
|
||||||
if (this.userId == -1) {
|
|
||||||
await getUserIdOfMailbox(this.boxId).then((res) => {
|
|
||||||
this.userId = res[0]?.user_id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.ownerId == this.userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async registerMembers(roomId: number) {
|
|
||||||
getAllMembers(this.messageId).then((res) => {
|
|
||||||
if (res.lenght == 0) return;
|
|
||||||
const data = res[0].id.split(",");
|
|
||||||
data.forEach(async (memberId: number) => {
|
|
||||||
await registerMember(roomId, memberId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async initiateRoom(owner: number, roomType: RoomType) {
|
|
||||||
try {
|
|
||||||
const roomId = await createRoom(this.envelope.subject, owner, this.messageId, roomType);
|
|
||||||
await registerMessageInRoom(this.messageId, roomId, this.envelope.date);
|
|
||||||
await this.registerMembers(roomId);
|
|
||||||
return roomId;
|
|
||||||
} catch (err) {
|
|
||||||
logger.err(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createOrRegisterOnExistence(owner: number, roomType: RoomType) {
|
|
||||||
await findRoomByOwner(owner).then(async (res) => {
|
|
||||||
if (res.length == 0) {
|
|
||||||
// first message with this sender
|
|
||||||
await this.initiateRoom(owner, roomType);
|
|
||||||
} else {
|
|
||||||
// not a reply, add to the list of message if this sender
|
|
||||||
await registerMessageInRoom(this.messageId, res[0].room_id, this.envelope.date);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async initiateThread() {
|
|
||||||
await createRoom(this.envelope.subject, this.ownerId, this.messageId, RoomType.THREAD).then(
|
|
||||||
async (threadId: number) => {
|
|
||||||
// find parent room infos
|
|
||||||
let roomId: number;
|
|
||||||
let root_id: number;
|
|
||||||
await getThreadInfo(this.inReplyTo).then(async (room) => {
|
|
||||||
// todo room not lenght, reply to transfer ?
|
|
||||||
roomId = room[0].room_id;
|
|
||||||
root_id = room[0].root_id;
|
|
||||||
if (root_id === undefined) root_id = roomId;
|
|
||||||
await registerThread(threadId, roomId, root_id);
|
|
||||||
});
|
|
||||||
// impl register previous message or go back
|
|
||||||
await registerMessageInRoom(this.messageId, threadId, this.envelope.date);
|
|
||||||
await this.registerMembers(threadId);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createOrRegisterOnMembers(roomId: number, isThread?: boolean) {
|
|
||||||
const hasSameMembers = await hasSameMembersAsParent(this.messageId, this.inReplyTo);
|
|
||||||
if (hasSameMembers) {
|
|
||||||
await registerMessageInRoom(this.messageId, roomId, this.envelope.date);
|
|
||||||
if (isThread) {
|
|
||||||
await getThreadInfoOnId(roomId).then(async (res) => {
|
|
||||||
let root_id = res[0].root_id;
|
|
||||||
if (root_id == undefined) root_id = res[0].room_id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await this.initiateThread();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
await this.init();
|
await this.init();
|
||||||
if (this.envelope.inReplyTo) {
|
if (this.envelope.inReplyTo) {
|
||||||
@@ -194,4 +107,98 @@ export default class RegisterMessageInApp {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (this.envelope.from) {
|
||||||
|
this.ownerId = await getAddressId(createAddress(this.envelope.from[0])); // todo use sender or from ?
|
||||||
|
} else {
|
||||||
|
throw new Error("Envelope must have a 'from' field");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isDm = () => nbMembers(this.envelope) == 2;
|
||||||
|
|
||||||
|
async isFromUs() {
|
||||||
|
if (this.userId == -1) {
|
||||||
|
await getUserIdOfMailbox(this.boxId).then((res) => {
|
||||||
|
this.userId = res[0]?.user_id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.ownerId == this.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add all members of the message to the room
|
||||||
|
*/
|
||||||
|
async registerMembers(roomId: number) {
|
||||||
|
getAllMembers(this.messageId).then((res) => {
|
||||||
|
if (res.lenght == 0) return;
|
||||||
|
const data = res[0].id.split(",");
|
||||||
|
data.forEach(async (memberId: number) => {
|
||||||
|
await registerMember(roomId, memberId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async initiateRoom(owner: number, roomType: RoomType) {
|
||||||
|
try {
|
||||||
|
const roomId = await createRoom(this.envelope.subject, owner, this.messageId, roomType);
|
||||||
|
await registerMessageInRoom(this.messageId, roomId, this.envelope.date);
|
||||||
|
await this.registerMembers(roomId);
|
||||||
|
return roomId;
|
||||||
|
} catch (err) {
|
||||||
|
logger.err(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrRegisterOnExistence(owner: number, roomType: RoomType) {
|
||||||
|
await findRoomByOwner(owner).then(async (res) => {
|
||||||
|
if (res.length == 0) {
|
||||||
|
// first message with this sender
|
||||||
|
await this.initiateRoom(owner, roomType);
|
||||||
|
} else {
|
||||||
|
// not a reply, add to the list of message if this sender
|
||||||
|
await registerMessageInRoom(this.messageId, res[0].room_id, this.envelope.date);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async initiateThread() {
|
||||||
|
const inReplyToId = (await getMessageIdOnID(this.inReplyTo))[0]?.message_id;
|
||||||
|
await createRoom(this.envelope.subject, this.ownerId, inReplyToId, RoomType.THREAD).then(
|
||||||
|
async (threadId: number) => {
|
||||||
|
// find parent room infos
|
||||||
|
let roomId: number;
|
||||||
|
let root_id: number;
|
||||||
|
await getThreadInfo(this.inReplyTo).then(async (room) => {
|
||||||
|
// todo room not lenght, reply to transfer ?
|
||||||
|
roomId = room[0].room_id;
|
||||||
|
root_id = room[0].root_id;
|
||||||
|
if (root_id === undefined) root_id = roomId;
|
||||||
|
await registerThread(threadId, roomId, root_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// add original message
|
||||||
|
await registerMessageInRoom(inReplyToId, threadId, this.envelope.date);
|
||||||
|
// add reply message
|
||||||
|
await registerMessageInRoom(this.messageId, threadId, this.envelope.date);
|
||||||
|
await this.registerMembers(threadId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrRegisterOnMembers(roomId: number, isThread?: boolean) {
|
||||||
|
const hasSameMembers = await hasSameMembersAsParent(this.messageId, this.inReplyTo);
|
||||||
|
if (hasSameMembers) {
|
||||||
|
await registerMessageInRoom(this.messageId, roomId, this.envelope.date);
|
||||||
|
if (isThread) {
|
||||||
|
await getThreadInfoOnId(roomId).then(async (res) => {
|
||||||
|
let root_id = res[0].root_id;
|
||||||
|
if (root_id == undefined) root_id = res[0].room_id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.initiateThread();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
back/mails/room/Room.ts
Normal file
46
back/mails/room/Room.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { deleteRoom, getRoomNbMessageAndThread, getRoomOnMessageId } from "../../db/Room-db";
|
||||||
|
|
||||||
|
export default class Room {
|
||||||
|
private _roomId: number;
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
setRoomId(roomId: number): Room {
|
||||||
|
this._roomId = roomId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get roomId(): number {
|
||||||
|
return this._roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setRoomIdOnMessageId(messageId: number): Promise<Room> {
|
||||||
|
const res = await getRoomOnMessageId(messageId);
|
||||||
|
if (res.length == 0) {
|
||||||
|
throw "Message has no room";
|
||||||
|
}
|
||||||
|
this._roomId = res[0].room_id;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the room have threads or messages
|
||||||
|
async shouldDelete(): Promise<boolean> {
|
||||||
|
if (!this._roomId) {
|
||||||
|
throw "shouldDelete needs to have a roomId set.";
|
||||||
|
}
|
||||||
|
const res = await getRoomNbMessageAndThread(this._roomId);
|
||||||
|
if (res.length === 0) return true;
|
||||||
|
if (res[0].nbMessage === 0 && res[0].nbThread === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(): Promise<Room> {
|
||||||
|
if (!this._roomId) {
|
||||||
|
throw "shouldDelete needs to have a roomId set.";
|
||||||
|
}
|
||||||
|
await deleteRoom(this._roomId);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,11 @@ export class SmtpInstance {
|
|||||||
transporter: Transporter;
|
transporter: Transporter;
|
||||||
user: string;
|
user: string;
|
||||||
|
|
||||||
constructor(account: { user: string; password: string }) {
|
constructor(account: { user: string; password: string, smtp_host: string, smtp_port: number }) {
|
||||||
// todo store other data
|
|
||||||
this.user = account.user;
|
this.user = account.user;
|
||||||
this.transporter = nodemailer.createTransport({
|
this.transporter = nodemailer.createTransport({
|
||||||
host: "smtp.gmail.com",
|
host: account.smtp_host,
|
||||||
port: 465,
|
port: account.smtp_port,
|
||||||
secure: true,
|
secure: true,
|
||||||
auth: {
|
auth: {
|
||||||
user: account.user,
|
user: account.user,
|
||||||
|
|||||||
@@ -49,4 +49,5 @@ export default class MailBuilder {
|
|||||||
this.message.subject = "RE: " + originSubject;
|
this.message.subject = "RE: " + originSubject;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
// https://cr.yp.to/immhf/thread.html
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import Message from "../abl/Message-abl";
|
import MessageAbl from "../abl/Message-abl";
|
||||||
import validator from "../validator/validator";
|
import validator from "../validator/validator";
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.post("/addFlag", async (req, res) => {
|
router.post("/addFlag", async (req, res) => {
|
||||||
await validator.validate("addFlag", req.body, res, Message.addFlag);
|
await validator.validate("addFlag", req.body, res, MessageAbl.addFlag);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/removeFlag", async (req, res) => {
|
router.post("/removeFlag", async (req, res) => {
|
||||||
await validator.validate("removeFlag", req.body, res, Message.removeFlag);
|
await validator.validate("removeFlag", req.body, res, MessageAbl.removeFlag);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/deleteRemote", async(req, res) => {
|
router.post("/deleteRemote", async(req, res) => {
|
||||||
await validator.validate("delete", req.body, res, Message.deleteRemoteOnly);
|
await validator.validate("delete", req.body, res, MessageAbl.deleteRemoteOnly);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/delete", async(req, res) => {
|
router.post("/delete", async(req, res) => {
|
||||||
await validator.validate("delete", req.body, res, Message.deleteEverywhere);
|
await validator.validate("delete", req.body, res, MessageAbl.deleteEverywhere);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import Room from "../abl/Room-abl";
|
import RoomAbl from "../abl/Room-abl";
|
||||||
import validator from "../validator/validator";
|
import validator from "../validator/validator";
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -7,18 +7,22 @@ const router = express.Router();
|
|||||||
* Return all messages from a room
|
* Return all messages from a room
|
||||||
*/
|
*/
|
||||||
router.get("/:roomId/messages", async (req, res) => {
|
router.get("/:roomId/messages", async (req, res) => {
|
||||||
await validator.validate("getMessages", req.params, res, Room.getMessages);
|
await validator.validate("getMessages", req.params, res, RoomAbl.getMessages);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return all members from a room
|
* Return all members from a room
|
||||||
*/
|
*/
|
||||||
router.get("/:roomId/members", async (req, res) => {
|
router.get("/:roomId/members", async (req, res) => {
|
||||||
await validator.validate("getMembers", req.params, res, Room.getMembers);
|
await validator.validate("getMembers", req.params, res, RoomAbl.getMembers);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/response", async (req, res) => {
|
router.post("/response", async (req, res) => {
|
||||||
await validator.validate("response", req.body, res, Room.response);
|
await validator.validate("response", req.body, res, RoomAbl.response);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/delete", async (req, res) => {
|
||||||
|
await validator.validate("deleteRoom", req.body, res, RoomAbl.delete);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
"pwd": { "type": "string" },
|
"pwd": { "type": "string" },
|
||||||
"xoauth": { "type": "string" },
|
"xoauth": { "type": "string" },
|
||||||
"xoauth2": { "type": "string" },
|
"xoauth2": { "type": "string" },
|
||||||
"host": { "type": "string", "format": "hostname" },
|
"imapHost": { "type": "string", "format": "hostname" },
|
||||||
"port": { "type": "number", "maximum": 65535 },
|
"smtpHost": { "type": "string", "format": "hostname" },
|
||||||
|
"imapPort": { "type": "number", "maximum": 65535 },
|
||||||
|
"smtpPort": { "type": "number", "maximum": 65535 },
|
||||||
"tls": { "type": "boolean" }
|
"tls": { "type": "boolean" }
|
||||||
},
|
},
|
||||||
"required": ["email", "host", "port", "tls"],
|
"required": ["email", "imapHost", "smtpHost", "imapPort", "smtpPort", "tls"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
12
back/validator/schemas/deleteRoom-schema.json
Normal file
12
back/validator/schemas/deleteRoom-schema.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"roomId": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"roomId"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import getMembersSchema from "./schemas/getMembers-schema.json";
|
|||||||
import setFlagSchema from "./schemas/setFlag-schema.json";
|
import setFlagSchema from "./schemas/setFlag-schema.json";
|
||||||
import responseSchema from "./schemas/response-schema.json";
|
import responseSchema from "./schemas/response-schema.json";
|
||||||
import deleteSchema from "./schemas/delete-schema.json";
|
import deleteSchema from "./schemas/delete-schema.json";
|
||||||
|
import deleteRoomSchema from "./schemas/deleteRoom-schema.json";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import statusCodes from "../utils/statusCodes";
|
import statusCodes from "../utils/statusCodes";
|
||||||
import logger from "../system/Logger";
|
import logger from "../system/Logger";
|
||||||
@@ -24,6 +25,7 @@ class Validator {
|
|||||||
validateSetFlag: any;
|
validateSetFlag: any;
|
||||||
validateResponse: any;
|
validateResponse: any;
|
||||||
delete: any;
|
delete: any;
|
||||||
|
deleteRoom: any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.validateCreateAccount = ajv.compile(createAccountSchema);
|
this.validateCreateAccount = ajv.compile(createAccountSchema);
|
||||||
@@ -34,6 +36,7 @@ class Validator {
|
|||||||
this.validateSetFlag = ajv.compile(setFlagSchema);
|
this.validateSetFlag = ajv.compile(setFlagSchema);
|
||||||
this.validateResponse = ajv.compile(responseSchema);
|
this.validateResponse = ajv.compile(responseSchema);
|
||||||
this.delete = ajv.compile(deleteSchema);
|
this.delete = ajv.compile(deleteSchema);
|
||||||
|
this.deleteRoom = ajv.compile(deleteRoomSchema);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getSchema(name: string): any {
|
_getSchema(name: string): any {
|
||||||
@@ -55,6 +58,8 @@ class Validator {
|
|||||||
return this.validateResponse;
|
return this.validateResponse;
|
||||||
case "delete":
|
case "delete":
|
||||||
return this.delete;
|
return this.delete;
|
||||||
|
case "deleteRoom":
|
||||||
|
return this.deleteRoom;
|
||||||
default:
|
default:
|
||||||
logger.err(`Schema ${name} not found`);
|
logger.err(`Schema ${name} not found`);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# mail
|
|
||||||
|
|
||||||
## Project setup
|
|
||||||
```
|
|
||||||
yarn install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compiles and hot-reloads for development
|
|
||||||
```
|
|
||||||
yarn serve
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compiles and minifies for production
|
|
||||||
```
|
|
||||||
yarn build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lints and fixes files
|
|
||||||
```
|
|
||||||
yarn lint
|
|
||||||
```
|
|
||||||
|
|
||||||
### Customize configuration
|
|
||||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
/* 343a46 */
|
/* 343a46 */
|
||||||
}
|
}
|
||||||
/* .badge-primary { */
|
/* .badge-primary { */
|
||||||
|
/* https://angel-rs.github.io/css-color-filter-generator/ */
|
||||||
|
|
||||||
.selected {
|
.selected {
|
||||||
background-color: var(--selected);
|
background-color: var(--selected);
|
||||||
|
|||||||
1
front/src/assets/svg/expand-left-fill.svg
Normal file
1
front/src/assets/svg/expand-left-fill.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10.4143 4.58594L10.4142 11.0003L16.0003 11.0004L16.0003 13.0004L10.4142 13.0003L10.4141 19.4144L3 12.0002L10.4143 4.58594ZM18.0002 19.0002V5.00018H20.0002V19.0002H18.0002Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 256 B |
1
front/src/assets/svg/pushpin-2-line.svg
Normal file
1
front/src/assets/svg/pushpin-2-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 3V5H17V11L19 14V16H13V23H11V16H5V14L7 11V5H6V3H18ZM9 5V11.6056L7.4037 14H16.5963L15 11.6056V5H9Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 184 B |
1
front/src/assets/svg/pushpin-line.svg
Normal file
1
front/src/assets/svg/pushpin-line.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.8273 1.68994L22.3126 10.1752L20.8984 11.5894L20.1913 10.8823L15.9486 15.125L15.2415 18.6605L13.8273 20.0747L9.58466 15.8321L4.63492 20.7818L3.2207 19.3676L8.17045 14.4179L3.92781 10.1752L5.34202 8.76101L8.87756 8.0539L13.1202 3.81126L12.4131 3.10416L13.8273 1.68994ZM14.5344 5.22548L9.86358 9.89631L7.0417 10.4607L13.5418 16.9608L14.1062 14.1389L18.7771 9.46812L14.5344 5.22548Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 466 B |
40
front/src/components/basic/Button.vue
Normal file
40
front/src/components/basic/Button.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
const props = defineProps({
|
||||||
|
onClick: { type: Function },
|
||||||
|
text: { type: String },
|
||||||
|
class: { type: String },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
<button :class="props.class" @click="props.onClick">{{ props.text }}</button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
button {
|
||||||
|
padding: 5px;
|
||||||
|
padding: 7px 18px;
|
||||||
|
background-color: #09a35b;
|
||||||
|
color: var(--primary-text);
|
||||||
|
border-radius: 5px;
|
||||||
|
border: none;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.danger {
|
||||||
|
background-color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cancel {
|
||||||
|
background-color: var(--quaternary-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
70
front/src/components/basic/Input.vue
Normal file
70
front/src/components/basic/Input.vue
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, defineEmits, withDefaults } from "vue";
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
type: string;
|
||||||
|
required: boolean;
|
||||||
|
onChange: any;
|
||||||
|
placeholder: string;
|
||||||
|
label: string;
|
||||||
|
vModel: string;
|
||||||
|
modelValue: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
required: () => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(["update:modelValue"]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
<label v-show="props.label">{{ props.label }}</label>
|
||||||
|
<input
|
||||||
|
:value="modelValue"
|
||||||
|
@input="$emit('update:modelValue', $event.target.value)"
|
||||||
|
:type="props.type"
|
||||||
|
:required="props.required"
|
||||||
|
@change="props.onChange"
|
||||||
|
:placeholder="props.placeholder"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
label {
|
||||||
|
color: var(--secondary-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
-webkit-box-flex: 1;
|
||||||
|
background-color: var(--quaternary-background);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--primary-text);
|
||||||
|
-ms-flex: 1;
|
||||||
|
flex: 1;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 400;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px 9px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
input[type="number"] {
|
||||||
|
appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chrome, Safari, Edge, Opera */
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
import { ref, computed, watchEffect } from "vue";
|
import { ref, computed, watchEffect } from "vue";
|
||||||
import Modal from "./Modal";
|
import Modal from "./Modal";
|
||||||
import API from "../../services/imapAPI";
|
import API from "../../services/imapAPI";
|
||||||
|
import Input from "../basic/Input.vue";
|
||||||
|
import Button from "../basic/Button.vue";
|
||||||
|
|
||||||
const modal = ref(false);
|
const modal = ref(false);
|
||||||
|
|
||||||
@@ -9,35 +11,49 @@ const email = ref("");
|
|||||||
const pwd = ref("");
|
const pwd = ref("");
|
||||||
const xoauth = ref("");
|
const xoauth = ref("");
|
||||||
const xoauth2 = ref("");
|
const xoauth2 = ref("");
|
||||||
const host = ref("");
|
const imapHost = ref("");
|
||||||
const port = ref(993);
|
const imapPort = ref(993);
|
||||||
|
const smtpHost = ref("");
|
||||||
|
const smtpPort = ref(465);
|
||||||
|
|
||||||
const error = ref("");
|
const error = ref("");
|
||||||
|
|
||||||
const knownHosts = {
|
const knownHosts = {
|
||||||
"outlook.com": {
|
"outlook.com": {
|
||||||
host: "outlook.office365.com",
|
imap: "outlook.office365.com",
|
||||||
|
smtp: "outlook.office365.com",
|
||||||
},
|
},
|
||||||
"hotmail.com": {
|
"hotmail.com": {
|
||||||
host: "outlook.office365.com",
|
imap: "outlook.office365.com",
|
||||||
|
smtp: "outlook.office365.com",
|
||||||
},
|
},
|
||||||
"live.com": {
|
"live.com": {
|
||||||
host: "outlook.office365.com",
|
imap: "outlook.office365.com",
|
||||||
|
smtp: "outlook.office365.com",
|
||||||
},
|
},
|
||||||
"zoho.com": {
|
"zoho.com": {
|
||||||
host: "imap.zoho.eu",
|
imap: "imap.zoho.eu",
|
||||||
|
smtp: "smtp.zoho.eu",
|
||||||
},
|
},
|
||||||
"yahoo.com": {
|
"yahoo.com": {
|
||||||
host: "imap.mail.yahoo.com",
|
imap: "imap.mail.yahoo.com",
|
||||||
|
smtp: "smtp.mail.yahoo.com",
|
||||||
},
|
},
|
||||||
"icloud.com": {
|
"icloud.com": {
|
||||||
host: "imap.mail.me.com",
|
imap: "imap.mail.me.com",
|
||||||
|
smtp: "smtp.mail.me.com",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const refHost = computed({
|
const refSmtpHost = computed({
|
||||||
set: (val) => {
|
set: (val) => {
|
||||||
host.value = val;
|
smtpHost.value = val;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const refImapHost = computed({
|
||||||
|
set: (val) => {
|
||||||
|
imapHost.value = val;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,7 +76,7 @@ function checkError() {
|
|||||||
err.value = "You need at least one authentification method.";
|
err.value = "You need at least one authentification method.";
|
||||||
} else if ([pwd.value, xoauth.value, xoauth2.value].filter((val) => val != "").length > 1) {
|
} else if ([pwd.value, xoauth.value, xoauth2.value].filter((val) => val != "").length > 1) {
|
||||||
err.value = "You need only one authentification method.";
|
err.value = "You need only one authentification method.";
|
||||||
} else if (host.value == "" || port.value == "") {
|
} else if (smtpHost.value == "" || smtpPort.value == "" || imapHost.value == "" || imapPort.value == "") {
|
||||||
err.value = "You need to complete the port and the host.";
|
err.value = "You need to complete the port and the host.";
|
||||||
} else {
|
} else {
|
||||||
err.value = "";
|
err.value = "";
|
||||||
@@ -75,8 +91,10 @@ function addAccountRequest() {
|
|||||||
pwd: pwd.value,
|
pwd: pwd.value,
|
||||||
xoauth: xoauth.value,
|
xoauth: xoauth.value,
|
||||||
xoauth2: xoauth2.value,
|
xoauth2: xoauth2.value,
|
||||||
host: host.value,
|
imapHost: imapHost.value,
|
||||||
port: port.value,
|
imapPort: imapPort.value,
|
||||||
|
smtpHost: smtpHost.value,
|
||||||
|
smtpPort: smtpPort.value,
|
||||||
tls: true,
|
tls: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,76 +116,75 @@ watchEffect(() => {
|
|||||||
function mailChange() {
|
function mailChange() {
|
||||||
if (email.value.includes("@")) {
|
if (email.value.includes("@")) {
|
||||||
const domain = email.value.split("@")[1];
|
const domain = email.value.split("@")[1];
|
||||||
if (!knownHosts[domain]) {
|
if (imapHost.value == "") {
|
||||||
refHost.value = "imap." + domain;
|
refImapHost.value = knownHosts[domain]?.imap ?? `imap.${domain}`;
|
||||||
} else {
|
}
|
||||||
refHost.value = knownHosts[domain].host;
|
if (smtpHost.value == "") {
|
||||||
|
refSmtpHost.value = knownHosts[domain]?.smtp ?? `smtp.${domain}`;
|
||||||
}
|
}
|
||||||
// todo check if manual
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<button @click="modal = true">Add Account</button>
|
<Button :onClick="() => (modal = true)" text="Add Account" />
|
||||||
<Modal v-if="modal" title="Add new account" @close-modal="modal = false">
|
<Modal v-if="modal" title="Add new account" @close-modal="modal = false">
|
||||||
<template v-slot:body>
|
<template v-slot:body>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Email: </label>
|
<Input
|
||||||
<input @change="mailChange" v-model="email" type="email" required />
|
label="Email:"
|
||||||
|
:onChange="mailChange"
|
||||||
|
v-model="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<fieldset>
|
<div class="field">
|
||||||
|
<Input label="Plain password:" v-model="pwd" type="password" />
|
||||||
|
</div>
|
||||||
|
<!-- <fieldset>
|
||||||
<legend>Authentification method</legend>
|
<legend>Authentification method</legend>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Plain password:</label>
|
<Input label="Xoauth:" v-model="xoauth" type="text" />
|
||||||
<input v-model="pwd" type="password" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Xoauth:</label>
|
<Input label="Xoauth2:" v-model="xoauth2" type="text" />
|
||||||
<input v-model="xoauth" type="text" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
</fieldset> -->
|
||||||
<label>Xoauth2:</label>
|
|
||||||
<input v-model="xoauth2" type="text" />
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<div class="config">
|
<fieldset class="config">
|
||||||
<input v-model="host" id="host" type="text" placeholder="imap host" />
|
<legend>Imap</legend>
|
||||||
<input v-model="port" id="port" type="number" placeholder="port" />
|
<Input v-model="imapHost" class="host" type="text" placeholder="host" />
|
||||||
</div>
|
<Input v-model="imapPort" class="port" type="number" placeholder="port" />
|
||||||
<!-- tls -->
|
</fieldset>
|
||||||
<div>
|
<fieldset class="config">
|
||||||
<button :disabled="error != ''" @click="addAccountRequest">Add</button>
|
<legend>Smtp</legend>
|
||||||
{{ error }}
|
<Input v-model="smtpHost" class="host" type="text" placeholder="host" />
|
||||||
</div>
|
<Input v-model="smtpPort" class="port" type="number" placeholder="port" />
|
||||||
|
</fieldset>
|
||||||
|
</template>
|
||||||
|
<!-- tls -->
|
||||||
|
<template v-slot:actions>
|
||||||
|
<Button :disabled="error != ''" :onClick="addAccountRequest" text="Add" />
|
||||||
|
{{ error }}
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style lang="scss">
|
||||||
/* Chrome, Safari, Edge, Opera */
|
|
||||||
input::-webkit-outer-spin-button,
|
|
||||||
input::-webkit-inner-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Firefox */
|
|
||||||
input[type="number"] {
|
|
||||||
appearance: textfield;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field > input {
|
.field {
|
||||||
margin-top: 2px;
|
input {
|
||||||
width: 95%;
|
margin-top: 2px;
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
@@ -179,54 +196,16 @@ fieldset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.config {
|
.config {
|
||||||
|
display: block;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#host {
|
.host {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
width: calc(95% - 100px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#port {
|
.port {
|
||||||
|
display: inline-flex;
|
||||||
width: 70px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 5px;
|
|
||||||
padding: 7px 18px;
|
|
||||||
background-color: #09a35b;
|
|
||||||
color: var(--primary-text);
|
|
||||||
border-radius: 5px;
|
|
||||||
border: none;
|
|
||||||
text-decoration: none;
|
|
||||||
display: inline-block;
|
|
||||||
transition: opacity 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
-webkit-box-flex: 1;
|
|
||||||
background-color: var(--quaternary-background);
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: var(--primary-text);
|
|
||||||
-ms-flex: 1;
|
|
||||||
flex: 1;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
font-weight: 400;
|
|
||||||
min-width: 0;
|
|
||||||
padding: 8px 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
color: var(--secondary-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
54
front/src/components/modals/ConfirmationModal.vue
Normal file
54
front/src/components/modals/ConfirmationModal.vue
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, defineProps, withDefaults } from "vue";
|
||||||
|
import Modal from "./Modal.vue";
|
||||||
|
import Button from "../basic/Button.vue";
|
||||||
|
|
||||||
|
const modal = ref(false);
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
title: string;
|
||||||
|
isDanger: boolean;
|
||||||
|
onConfirmation: any;
|
||||||
|
onCancel?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: () => "Confirmation",
|
||||||
|
isDanger: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = ref("");
|
||||||
|
|
||||||
|
const handleContinue = async () => {
|
||||||
|
try {
|
||||||
|
if (props.onConfirmation) {
|
||||||
|
await props.onConfirmation();
|
||||||
|
}
|
||||||
|
modal.value = false;
|
||||||
|
} catch (err: any) {
|
||||||
|
error.value = err.message;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = async () => {
|
||||||
|
if (props.onCancel) {
|
||||||
|
await props.onCancel();
|
||||||
|
}
|
||||||
|
modal.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Modal v-if="modal" :title="props.title" @close-modal="modal = false">
|
||||||
|
<template v-slot:body> Are you sure you want to do that ? </template>
|
||||||
|
<template v-slot:actions>
|
||||||
|
<Button :onClick="handleCancel" class="cancel" text="Cancel" />
|
||||||
|
<Button :onClick="handleContinue" :class="props.isDanger ? 'danger' : ''" text="Confirm" />
|
||||||
|
{{ error }}
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -1,26 +1,59 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Message } from "@/store/models/model";
|
import { Address, Message, Room } from "@/store/models/model";
|
||||||
import { ref, watch, defineProps, PropType } from "vue";
|
import { ref, Ref, watch, defineProps, PropType } from "vue";
|
||||||
import Content from "../structure/message/Content.vue";
|
import Content from "../structure/message/Content.vue";
|
||||||
import Modal from "./Modal.vue";
|
import Modal from "./Modal.vue";
|
||||||
|
import MemberList from "@/views/room/MemberList.vue";
|
||||||
|
import { removeDuplicates } from "@/utils/array";
|
||||||
|
import { decodeEmojis } from "@/utils/string";
|
||||||
|
|
||||||
const props = defineProps({ messageId: { type: Number, require: true }, message: Object as PropType<Message> });
|
const props = defineProps({
|
||||||
|
messageId: { type: Number, require: true },
|
||||||
|
message: Object as PropType<Message>,
|
||||||
|
room: Object as PropType<Room>,
|
||||||
|
});
|
||||||
|
|
||||||
const messageId = ref(-1);
|
const messageId = ref(-1);
|
||||||
|
const fromA = ref<Address[]>([]);
|
||||||
|
const toA = ref<Address[]>([]);
|
||||||
|
const ccA = ref<Address[]>([]);
|
||||||
|
|
||||||
|
const getAddr = (ids: string | undefined, addrs: Address[] | undefined, res: Ref<Address[]>) => {
|
||||||
|
res.value = [];
|
||||||
|
if (!ids || !addrs) return;
|
||||||
|
let idsClean = removeDuplicates(ids.split(","));
|
||||||
|
idsClean.forEach((id) => {
|
||||||
|
let addr = addrs.find((member) => member.id === parseInt(id));
|
||||||
|
if (addr) {
|
||||||
|
res.value.push(addr);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.messageId,
|
() => props.messageId,
|
||||||
(newMessageId: number | undefined) => {
|
(newMessageId: number | undefined) => {
|
||||||
if (!newMessageId) return;
|
if (!newMessageId) return;
|
||||||
messageId.value = newMessageId;
|
messageId.value = newMessageId;
|
||||||
|
getAddr(props.message?.fromA, props.room?.members, fromA);
|
||||||
|
getAddr(props.message?.toA, props.room?.members, toA);
|
||||||
|
getAddr(props.message?.ccA, props.room?.members, ccA);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<Modal v-if="messageId != -1" @close-modal="() => $emit('close')">
|
<Modal v-if="messageId != -1" @close-modal="() => $emit('close')" id="modal">
|
||||||
|
<template v-slot:header>
|
||||||
|
<h2 id="header">
|
||||||
|
{{ decodeEmojis(props.message?.subject) ?? "No Object" }}
|
||||||
|
</h2>
|
||||||
|
</template>
|
||||||
<template v-slot:body>
|
<template v-slot:body>
|
||||||
<div>{{ props.message?.subject ?? "No Object" }}</div>
|
<MemberList type="from" :members="fromA" />
|
||||||
|
<MemberList type="to" :members="toA" />
|
||||||
|
<MemberList v-if="ccA.length > 0" type="cc" :members="ccA" />
|
||||||
<Content type="large" :content="props.message?.content" class="content" />
|
<Content type="large" :content="props.message?.content" class="content" />
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -31,6 +64,11 @@ watch(
|
|||||||
.main {
|
.main {
|
||||||
min-width: 700px;
|
min-width: 700px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
/* todo define size automatically */
|
/* todo define size automatically */
|
||||||
.content {
|
.content {
|
||||||
width: 700px;
|
width: 700px;
|
||||||
|
|||||||
@@ -26,13 +26,18 @@ onUnmounted(() => {
|
|||||||
<div class="modal-wrapper">
|
<div class="modal-wrapper">
|
||||||
<div class="modal" v-on-click-outside="close">
|
<div class="modal" v-on-click-outside="close">
|
||||||
<header class="modal-header">
|
<header class="modal-header">
|
||||||
<h2>{{ props.title }}</h2>
|
<slot name="header">
|
||||||
|
<h2>{{ props.title }}</h2>
|
||||||
|
</slot>
|
||||||
<div class="close-button" @click="close"></div>
|
<div class="close-button" @click="close"></div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<slot name="body"> This is the default body! </slot>
|
<slot name="body"> This is the default body! </slot>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<slot name="actions"></slot>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -58,13 +63,29 @@ onUnmounted(() => {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
color: var(--primary-text);
|
color: var(--primary-text);
|
||||||
background-color: var(--secondary-background);
|
background-color: var(--secondary-background);
|
||||||
padding: 20px;
|
padding: 10px;
|
||||||
|
min-width: 220px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 2.4rem;
|
font-size: 2.4rem;
|
||||||
|
|||||||
@@ -84,6 +84,19 @@ function sendMessage() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getClasses = (isActive, disabled = false) => {
|
||||||
|
let classes = [];
|
||||||
|
if (isActive) {
|
||||||
|
classes.push("is-active");
|
||||||
|
}
|
||||||
|
if (!disabled) {
|
||||||
|
classes.push("selectable");
|
||||||
|
} else {
|
||||||
|
classes.push("disabled");
|
||||||
|
}
|
||||||
|
return classes.join(",");
|
||||||
|
};
|
||||||
|
|
||||||
// todo subject input when dm of group...
|
// todo subject input when dm of group...
|
||||||
// Font selection: choose the font family, size, color, and style
|
// Font selection: choose the font family, size, color, and style
|
||||||
// Images: insert pictures and graphics into your email
|
// Images: insert pictures and graphics into your email
|
||||||
@@ -99,31 +112,31 @@ function sendMessage() {
|
|||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="bold"
|
svg="bold"
|
||||||
@click="editor.chain().focus().toggleBold().run()"
|
@click="editor.chain().focus().toggleBold().run()"
|
||||||
:class="[{ 'is-active': editor.isActive('bold') }, 'editorOption']"
|
:classes="getClasses(editor.isActive('bold'))"
|
||||||
v-tooltip="{ text: 'Bold', shortcut: ['Ctrl', 'B'] }"
|
v-tooltip="{ text: 'Bold', shortcut: ['Ctrl', 'B'] }"
|
||||||
/>
|
/>
|
||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="italic"
|
svg="italic"
|
||||||
@click="editor.chain().focus().toggleItalic().run()"
|
@click="editor.chain().focus().toggleItalic().run()"
|
||||||
:class="[{ 'is-active': editor.isActive('italic') }, 'editorOption']"
|
:classes="getClasses(editor.isActive('italic'))"
|
||||||
v-tooltip="{ text: 'Italic', shortcut: ['Ctrl', 'I'] }"
|
v-tooltip="{ text: 'Italic', shortcut: ['Ctrl', 'I'] }"
|
||||||
/>
|
/>
|
||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="strikethrough"
|
svg="strikethrough"
|
||||||
@click="editor.chain().focus().toggleStrike().run()"
|
@click="editor.chain().focus().toggleStrike().run()"
|
||||||
:class="[{ 'is-active': editor.isActive('strike') }, 'editorOption']"
|
:classes="getClasses(editor.isActive('strike'))"
|
||||||
v-tooltip="{ text: 'Strike', shortcut: ['Ctrl', 'Shift', 'X'] }"
|
v-tooltip="{ text: 'Strike', shortcut: ['Ctrl', 'Shift', 'X'] }"
|
||||||
/>
|
/>
|
||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="underline"
|
svg="underline"
|
||||||
@click="editor.chain().focus().toggleUnderline().run()"
|
@click="editor.chain().focus().toggleUnderline().run()"
|
||||||
:class="[{ 'is-active': editor.isActive('underline') }, 'editorOption']"
|
:classes="getClasses(editor.isActive('underline'))"
|
||||||
v-tooltip="{ text: 'Underline', shortcut: ['Ctrl', 'U'] }"
|
v-tooltip="{ text: 'Underline', shortcut: ['Ctrl', 'U'] }"
|
||||||
/>
|
/>
|
||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="font-color"
|
svg="font-color"
|
||||||
@click="editor.commands.toggleHighlight({ color: '#ffcc00' })"
|
@click="editor.commands.toggleHighlight({ color: '#ffcc00' })"
|
||||||
:class="[{ 'is-active': editor.isActive('highlight') }, 'editorOption']"
|
:classes="getClasses(editor.isActive('highlight'))"
|
||||||
v-tooltip="{ text: 'Highlight', shortcut: ['Ctrl', 'Shift', 'H'] }"
|
v-tooltip="{ text: 'Highlight', shortcut: ['Ctrl', 'Shift', 'H'] }"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -131,19 +144,19 @@ function sendMessage() {
|
|||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="h-2"
|
svg="h-2"
|
||||||
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
|
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
|
||||||
:class="[{ 'is-active': editor.isActive({ level: 2 }) }, 'editorOption']"
|
:classes="getClasses(editor.isActive({ level: 2 }))"
|
||||||
v-tooltip="{ text: 'Heading 2', shortcut: ['Ctrl', 'Alt', '2'] }"
|
v-tooltip="{ text: 'Heading 2', shortcut: ['Ctrl', 'Alt', '2'] }"
|
||||||
/>
|
/>
|
||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="h-3"
|
svg="h-3"
|
||||||
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
|
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
|
||||||
:class="[{ 'is-active': editor.isActive({ level: 3 }) }, 'editorOption']"
|
:classes="getClasses(editor.isActive({ level: 3 }))"
|
||||||
v-tooltip="{ text: 'Heading 3', shortcut: ['Ctrl', 'Alt', '3'] }"
|
v-tooltip="{ text: 'Heading 3', shortcut: ['Ctrl', 'Alt', '3'] }"
|
||||||
/>
|
/>
|
||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="h-4"
|
svg="h-4"
|
||||||
@click="editor.chain().focus().toggleHeading({ level: 4 }).run()"
|
@click="editor.chain().focus().toggleHeading({ level: 4 }).run()"
|
||||||
:class="[{ 'is-active': editor.isActive({ level: 4 }) }, 'editorOption']"
|
:classes="getClasses(editor.isActive({ level: 4 }))"
|
||||||
v-tooltip="{ text: 'Heading 4', shortcut: ['Ctrl', 'Alt', '4'] }"
|
v-tooltip="{ text: 'Heading 4', shortcut: ['Ctrl', 'Alt', '4'] }"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -156,13 +169,13 @@ function sendMessage() {
|
|||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="list-ordered"
|
svg="list-ordered"
|
||||||
@click="editor.chain().focus().toggleOrderedList().run()"
|
@click="editor.chain().focus().toggleOrderedList().run()"
|
||||||
:class="[{ 'is-active': editor.isActive('orderedList') }, 'editorOption']"
|
:classes="getClasses(editor.isActive('orderedList'))"
|
||||||
v-tooltip="{ text: 'Ordered List', shortcut: ['Ctrl', 'Alt', '7'] }"
|
v-tooltip="{ text: 'Ordered List', shortcut: ['Ctrl', 'Alt', '7'] }"
|
||||||
/>
|
/>
|
||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="list-unordered"
|
svg="list-unordered"
|
||||||
@click="editor.chain().focus().toggleBulletList().run()"
|
@click="editor.chain().focus().toggleBulletList().run()"
|
||||||
:class="[{ 'is-active': editor.isActive('bulletList') }, 'editorOption']"
|
:classes="getClasses(editor.isActive('bulletList'))"
|
||||||
v-tooltip="{ text: 'Unordered List', shortcut: ['Ctrl', 'Alt', '8'] }"
|
v-tooltip="{ text: 'Unordered List', shortcut: ['Ctrl', 'Alt', '8'] }"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -170,26 +183,26 @@ function sendMessage() {
|
|||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="align-left"
|
svg="align-left"
|
||||||
@click="editor.chain().focus().setTextAlign('left').run()"
|
@click="editor.chain().focus().setTextAlign('left').run()"
|
||||||
:class="[{ 'is-active': editor.isActive({ textAlign: 'left' }) }, 'editorOption']"
|
:classes="getClasses(editor.isActive({ textAlign: 'left' }))"
|
||||||
v-tooltip="{ text: 'Align Left', shortcut: ['Ctrl', 'Shift', 'L'] }"
|
v-tooltip="{ text: 'Align Left', shortcut: ['Ctrl', 'Shift', 'L'] }"
|
||||||
/>
|
/>
|
||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="align-center"
|
svg="align-center"
|
||||||
@click="editor.chain().focus().setTextAlign('center').run()"
|
@click="editor.chain().focus().setTextAlign('center').run()"
|
||||||
:class="[{ 'is-active': editor.isActive({ textAlign: 'center' }) }, 'editorOption']"
|
:classes="getClasses(editor.isActive({ textAlign: 'center' }))"
|
||||||
v-tooltip="{ text: 'Align Left', shortcut: ['Ctrl', 'Shift', 'E'] }"
|
v-tooltip="{ text: 'Align Center', shortcut: ['Ctrl', 'Shift', 'E'] }"
|
||||||
/>
|
/>
|
||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="align-right"
|
svg="align-right"
|
||||||
@click="editor.chain().focus().setTextAlign('right').run()"
|
@click="editor.chain().focus().setTextAlign('right').run()"
|
||||||
:class="[{ 'is-active': editor.isActive({ textAlign: 'right' }) }, 'editorOption']"
|
:classes="getClasses(editor.isActive({ textAlign: 'right' }))"
|
||||||
v-tooltip="{ text: 'Align Left', shortcut: ['Ctrl', 'Shift', 'R'] }"
|
v-tooltip="{ text: 'Align Right', shortcut: ['Ctrl', 'Shift', 'R'] }"
|
||||||
/>
|
/>
|
||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="align-justify"
|
svg="align-justify"
|
||||||
@click="editor.chain().focus().setTextAlign('justify').run()"
|
@click="editor.chain().focus().setTextAlign('justify').run()"
|
||||||
:class="[{ 'is-active': editor.isActive({ textAlign: 'justify' }) }, 'editorOption']"
|
:classes="getClasses(editor.isActive({ textAlign: 'justify' }))"
|
||||||
v-tooltip="{ text: 'Align Left', shortcut: ['Ctrl', 'Shift', 'J'] }"
|
v-tooltip="{ text: 'Justify', shortcut: ['Ctrl', 'Shift', 'J'] }"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -207,14 +220,14 @@ function sendMessage() {
|
|||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="indent-increase"
|
svg="indent-increase"
|
||||||
@click="!$event.disabled ? editor.chain().focus().sinkListItem('listItem').run() : ''"
|
@click="!$event.disabled ? editor.chain().focus().sinkListItem('listItem').run() : ''"
|
||||||
:class="[{ disabled: !editor.can().sinkListItem('listItem') }, 'editorOption']"
|
:classes="getClasses(false, !editor.can().sinkListItem('listItem'))"
|
||||||
:isDisabled="!editor.can().sinkListItem('listItem')"
|
:isDisabled="!editor.can().sinkListItem('listItem')"
|
||||||
v-tooltip="{ text: 'Sink Item', shortcut: ['Tab'] }"
|
v-tooltip="{ text: 'Sink Item', shortcut: ['Tab'] }"
|
||||||
/>
|
/>
|
||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="indent-decrease"
|
svg="indent-decrease"
|
||||||
@click="!$event.disabled ? editor.chain().focus().liftListItem('listItem').run() : ''"
|
@click="!$event.disabled ? editor.chain().focus().liftListItem('listItem').run() : ''"
|
||||||
:class="[{ disabled: !editor.can().liftListItem('listItem') }, 'editorOption']"
|
:classes="getClasses(false, !editor.can().liftListItem('listItem'))"
|
||||||
:isDisabled="!editor.can().liftListItem('listItem')"
|
:isDisabled="!editor.can().liftListItem('listItem')"
|
||||||
v-tooltip="{ text: 'Lift Item', shortcut: ['Shift', 'Tab'] }"
|
v-tooltip="{ text: 'Lift Item', shortcut: ['Shift', 'Tab'] }"
|
||||||
/>
|
/>
|
||||||
@@ -238,7 +251,8 @@ function sendMessage() {
|
|||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="italic"
|
svg="italic"
|
||||||
@click="editor.chain().focus().toggleItalic().run()"
|
@click="editor.chain().focus().toggleItalic().run()"
|
||||||
:class="[{ 'is-active': editor.isActive('italic') }, 'editorOption']"
|
:class="[{ 'is-active,selectable': editor.isActive('italic') }, 'editorOption']"
|
||||||
|
:classes="[{ 'is-active,selectable': editor.isActive('italic') }, 'selectable'].join()"
|
||||||
/>
|
/>
|
||||||
<SvgLoader
|
<SvgLoader
|
||||||
svg="strikethrough"
|
svg="strikethrough"
|
||||||
@@ -321,20 +335,6 @@ function sendMessage() {
|
|||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editorOption {
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&.is-active {
|
|
||||||
background-color: var(--selected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, withDefaults, ref } from "vue";
|
import { defineProps, withDefaults, ref } from "vue";
|
||||||
import { decodeEmojis } from "../../../utils/string";
|
import { decodeEmojis } from "../../../utils/string";
|
||||||
import { removeDuplicates } from "../../../utils/array";
|
|
||||||
import { Address, Message } from "@/store/models/model";
|
import { Address, Message } from "@/store/models/model";
|
||||||
import Content from "./Content.vue";
|
import Content from "./Content.vue";
|
||||||
import Options from "./Options.vue";
|
import Options from "./Options.vue";
|
||||||
import { isSeenFc } from "@/utils/flagsUtils";
|
import { isSeenFc } from "@/utils/flagsUtils";
|
||||||
import SvgLoader from "@/components/utils/SvgLoader.vue";
|
import SvgLoader from "@/components/utils/SvgLoader.vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { displayAddresses } from "@/utils/address";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
msg: Message;
|
msg: Message;
|
||||||
@@ -20,17 +21,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
|
|
||||||
const compact = ref(props.compact);
|
const compact = ref(props.compact);
|
||||||
|
|
||||||
const displayAddresses = (addressIds: string[] | undefined): string => {
|
|
||||||
if (!addressIds) return "";
|
|
||||||
addressIds = removeDuplicates(addressIds);
|
|
||||||
let res = "";
|
|
||||||
addressIds.forEach((addressId) => {
|
|
||||||
const address = props.members?.find((member) => member.id === parseInt(addressId));
|
|
||||||
if (address) res += address.email;
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
const classes = (): string => {
|
const classes = (): string => {
|
||||||
const flags = props.msg?.flags;
|
const flags = props.msg?.flags;
|
||||||
|
|
||||||
@@ -43,6 +33,17 @@ const classes = (): string => {
|
|||||||
}
|
}
|
||||||
return "msg-basic";
|
return "msg-basic";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const style = (prop: string): string => {
|
||||||
|
// get color of the member that send the message
|
||||||
|
let member = props.members?.find((member) => member.id === parseInt(props.msg?.fromA?.split(",")[0]));
|
||||||
|
if (member?.color) {
|
||||||
|
return `${prop}: ${member.color} !important`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
</script>
|
</script>
|
||||||
<!-- to if to is more than me
|
<!-- to if to is more than me
|
||||||
cc -->
|
cc -->
|
||||||
@@ -51,9 +52,9 @@ const classes = (): string => {
|
|||||||
attachments -->
|
attachments -->
|
||||||
<template>
|
<template>
|
||||||
<div class="message" @dblclick="$emit('openMessageView', props.msg?.id)">
|
<div class="message" @dblclick="$emit('openMessageView', props.msg?.id)">
|
||||||
<div id="context">
|
<div id="context" :style="style('background-color')">
|
||||||
<div class="left" id="profile">
|
<div class="left" id="profile">
|
||||||
{{ displayAddresses(props.msg?.fromA?.split(",")) }} - {{ props.msg?.fromA }}
|
{{ displayAddresses(props.msg?.fromA?.split(","), props.members) }} - {{ props.msg?.fromA }}
|
||||||
</div>
|
</div>
|
||||||
<div class="middle">{{ decodeEmojis(props.msg?.subject) }}</div>
|
<div class="middle">{{ decodeEmojis(props.msg?.subject) }}</div>
|
||||||
<div class="right" id="date">
|
<div class="right" id="date">
|
||||||
@@ -70,7 +71,7 @@ const classes = (): string => {
|
|||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content" :class="[classes()]">
|
<div class="content" :class="[classes()]" :style="style('border-color')">
|
||||||
<Content v-if="!compact" :content="props.msg?.content" />
|
<Content v-if="!compact" :content="props.msg?.content" />
|
||||||
<SvgLoader
|
<SvgLoader
|
||||||
v-if="compact"
|
v-if="compact"
|
||||||
@@ -81,16 +82,33 @@ const classes = (): string => {
|
|||||||
/>
|
/>
|
||||||
<Options class="options" :msg="props.msg" />
|
<Options class="options" :msg="props.msg" />
|
||||||
</div>
|
</div>
|
||||||
|
<div id="thread-link" v-if="props.msg?.thread" @click="router.push(`/${props.msg?.thread}`)">
|
||||||
|
<SvgLoader svg="expand-left-fill" />
|
||||||
|
<span>Go to the full conversation.</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="scss">
|
||||||
.message {
|
.message {
|
||||||
width: auto;
|
width: auto;
|
||||||
/* border: white 1px solid; */
|
/* border: white 1px solid; */
|
||||||
margin: 10px 5px 0px 5px;
|
margin: 10px 5px 0px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#thread-link {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--selected);
|
||||||
|
}
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--quaternary-background);
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
#context {
|
#context {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -112,8 +130,11 @@ const classes = (): string => {
|
|||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding-top: 6px;
|
padding: 6px;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
border: solid 4px var(--quaternary-background);
|
||||||
|
border-top: 0;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expand-contract {
|
.expand-contract {
|
||||||
|
|||||||
@@ -144,16 +144,4 @@ const deleteRemoteOnly = () => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
|
||||||
border: solid 1px;
|
|
||||||
border-radius: 6px;
|
|
||||||
display: initial;
|
|
||||||
padding: 1px 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--selected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -21,22 +21,25 @@ const classes = () => props.classes?.split(",") ?? "";
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.mainSvg {
|
.mainSvg {
|
||||||
display: inherit;
|
display: flex;
|
||||||
min-width: 26px;
|
&.selectable {
|
||||||
min-height: 26px;
|
display: inline-block;
|
||||||
}
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
.mainSvg.selectable {
|
&:hover,
|
||||||
border-radius: 6px;
|
&.is-active {
|
||||||
cursor: pointer;
|
background-color: var(--selected);
|
||||||
&:hover,
|
}
|
||||||
&.is-active {
|
}
|
||||||
background-color: var(--selected);
|
.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
|
min-width: 26px;
|
||||||
|
min-height: 26px;
|
||||||
filter: var(--svg-primary-text);
|
filter: var(--svg-primary-text);
|
||||||
&.danger {
|
&.danger {
|
||||||
filter: var(--svg-danger);
|
filter: var(--svg-danger);
|
||||||
|
|||||||
@@ -31,4 +31,7 @@ export default {
|
|||||||
deleteEverywhere(data: { mailboxId: number; messageId: number }) {
|
deleteEverywhere(data: { mailboxId: number; messageId: number }) {
|
||||||
return API().post(`/message/delete`, data);
|
return API().post(`/message/delete`, data);
|
||||||
},
|
},
|
||||||
|
deleteRoom(id: number) {
|
||||||
|
return API().post(`/room/delete`, { roomId: id });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface Message {
|
|||||||
content: string;
|
content: string;
|
||||||
date: string;
|
date: string;
|
||||||
flags: string[];
|
flags: string[];
|
||||||
|
thread: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LoadingState {
|
export enum LoadingState {
|
||||||
@@ -33,6 +34,7 @@ export interface Room {
|
|||||||
members: Address[];
|
members: Address[];
|
||||||
notSeen: number;
|
notSeen: number;
|
||||||
threadIds: number[];
|
threadIds: number[];
|
||||||
|
parent_id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Account {
|
export interface Account {
|
||||||
@@ -45,5 +47,6 @@ export interface Address {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
email: string;
|
email: string;
|
||||||
|
color: string | undefined;
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { decodeEmojis } from "@/utils/string";
|
|||||||
import { AxiosError, AxiosResponse } from "axios";
|
import { AxiosError, AxiosResponse } from "axios";
|
||||||
import { createStore } from "vuex";
|
import { createStore } from "vuex";
|
||||||
import { Room, Account, Address, RoomType, Message, LoadingState } from "./models/model";
|
import { Room, Account, Address, RoomType, Message, LoadingState } from "./models/model";
|
||||||
|
import { removeDuplicates } from "@/utils/array";
|
||||||
|
|
||||||
interface RoomFromBack {
|
interface RoomFromBack {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -32,6 +33,7 @@ function createRoom(options: RoomFromBack): Room {
|
|||||||
user: options.user,
|
user: options.user,
|
||||||
notSeen: options.notSeen,
|
notSeen: options.notSeen,
|
||||||
threadIds: [],
|
threadIds: [],
|
||||||
|
parent_id: options.parent_id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,8 +78,6 @@ function updateSeen(state: State, roomId: number, flag: string[], adding: boolea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// define injection key todo
|
|
||||||
// export const key: InjectionKey<Store<State>> = Symbol()
|
|
||||||
|
|
||||||
const store = createStore<State>({
|
const store = createStore<State>({
|
||||||
state: {
|
state: {
|
||||||
@@ -95,9 +95,6 @@ const store = createStore<State>({
|
|||||||
},
|
},
|
||||||
setActiveRoom(state, payload) {
|
setActiveRoom(state, payload) {
|
||||||
state.activeRoom = payload;
|
state.activeRoom = payload;
|
||||||
// todo load room on load page
|
|
||||||
const room = roomOnId(state, payload);
|
|
||||||
if (!room) return;
|
|
||||||
let roomMessage = msgOnRoomId(state, payload);
|
let roomMessage = msgOnRoomId(state, payload);
|
||||||
if (!roomMessage) {
|
if (!roomMessage) {
|
||||||
state.roomMessages.push({ messages: [], fetch: LoadingState.notLoaded, roomId: payload });
|
state.roomMessages.push({ messages: [], fetch: LoadingState.notLoaded, roomId: payload });
|
||||||
@@ -114,7 +111,6 @@ const store = createStore<State>({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
addRooms(state, payload) {
|
addRooms(state, payload) {
|
||||||
// todo add if not exist
|
|
||||||
const buffer: RoomFromBack[] = [];
|
const buffer: RoomFromBack[] = [];
|
||||||
payload.rooms.forEach((room: RoomFromBack) => {
|
payload.rooms.forEach((room: RoomFromBack) => {
|
||||||
if (room.roomType == RoomType.THREAD) {
|
if (room.roomType == RoomType.THREAD) {
|
||||||
@@ -136,23 +132,33 @@ const store = createStore<State>({
|
|||||||
const roomMessageIndex = state.roomMessages.findIndex((roomM) => roomM.roomId === payload.roomId);
|
const roomMessageIndex = state.roomMessages.findIndex((roomM) => roomM.roomId === payload.roomId);
|
||||||
state.roomMessages.splice(roomMessageIndex, 1);
|
state.roomMessages.splice(roomMessageIndex, 1);
|
||||||
const roomIndex = state.rooms.findIndex((room) => room.id === payload.roomId);
|
const roomIndex = state.rooms.findIndex((room) => room.id === payload.roomId);
|
||||||
|
const roomToDelete = state.rooms[roomIndex];
|
||||||
|
// todo debug parent_id to root_id
|
||||||
|
// remove thread
|
||||||
|
if (roomToDelete.roomType === RoomType.THREAD && roomToDelete.parent_id) {
|
||||||
|
const parentRoom = roomOnId(state, roomToDelete.parent_id);
|
||||||
|
if (parentRoom) {
|
||||||
|
parentRoom.threadIds = parentRoom?.threadIds.filter((id) => id !== roomToDelete.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// todo fix to many at the same time bug
|
||||||
state.rooms.splice(roomIndex, 1);
|
state.rooms.splice(roomIndex, 1);
|
||||||
// state.activeRoom = state.rooms[0].id;
|
// state.activeRoom = state.rooms[0].id;
|
||||||
// router.push(`/${state.activeRoom}`);
|
// router.push(`/${state.activeRoom}`);
|
||||||
},
|
},
|
||||||
addMessages(state, payload) {
|
addMessages(state, payload) {
|
||||||
// todo add if not exist
|
|
||||||
const room = roomOnId(state, payload.roomId);
|
|
||||||
if (!room) return;
|
|
||||||
let roomMessage = msgOnRoomId(state, payload.roomId);
|
let roomMessage = msgOnRoomId(state, payload.roomId);
|
||||||
if (!roomMessage) {
|
if (!roomMessage) {
|
||||||
state.roomMessages.push({ roomId: payload.roomId, messages: [], fetch: LoadingState.notLoaded });
|
state.roomMessages.push({ roomId: payload.roomId, messages: [], fetch: LoadingState.notLoaded });
|
||||||
roomMessage = msgOnRoomId(state, payload.roomId);
|
roomMessage = msgOnRoomId(state, payload.roomId);
|
||||||
}
|
}
|
||||||
if (!roomMessage) return;
|
if (!roomMessage) return;
|
||||||
|
|
||||||
payload.messages.forEach((message: any) => {
|
payload.messages.forEach((message: any) => {
|
||||||
message.flags = message.flags?.split(",") ?? [];
|
message.flags = message.flags?.split(",") ?? [];
|
||||||
|
// todo fix upstream
|
||||||
|
// message.fromA = message.fromA ? removeDuplicates(message.fromA.split(",").join(",")) : null;
|
||||||
|
// message.toA = message.toA ? removeDuplicates(message.toA.split(",").join(",")) : null;
|
||||||
|
// message.ccA = message.ccA ? removeDuplicates(message.ccA.split(",").join(",")) : null;
|
||||||
roomMessage?.messages.push(message);
|
roomMessage?.messages.push(message);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -214,9 +220,14 @@ const store = createStore<State>({
|
|||||||
messages:
|
messages:
|
||||||
(state) =>
|
(state) =>
|
||||||
(roomId: number): Message[] => {
|
(roomId: number): Message[] => {
|
||||||
|
if (!roomId) return [];
|
||||||
const roomMessage = msgOnRoomId(state, roomId);
|
const roomMessage = msgOnRoomId(state, roomId);
|
||||||
if (roomMessage) return roomMessage.messages;
|
if (roomMessage && roomMessage.fetch === LoadingState.loaded) return roomMessage.messages;
|
||||||
state.roomMessages.push({ messages: [], roomId: roomId, fetch: LoadingState.notLoaded });
|
if (!roomMessage) {
|
||||||
|
state.roomMessages.push({ messages: [], roomId: roomId, fetch: LoadingState.notLoaded });
|
||||||
|
} else if (roomMessage.fetch === LoadingState.loaded) {
|
||||||
|
return roomMessage.messages;
|
||||||
|
}
|
||||||
store.dispatch("fetchMessages", { roomId: roomId, obj: msgOnRoomId(state, roomId) });
|
store.dispatch("fetchMessages", { roomId: roomId, obj: msgOnRoomId(state, roomId) });
|
||||||
return msgOnRoomId(state, roomId)?.messages ?? [];
|
return msgOnRoomId(state, roomId)?.messages ?? [];
|
||||||
},
|
},
|
||||||
|
|||||||
13
front/src/utils/address.ts
Normal file
13
front/src/utils/address.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Address } from "@/store/models/model";
|
||||||
|
import { removeDuplicates } from "./array";
|
||||||
|
|
||||||
|
export const displayAddresses = (addressIds: string[] | undefined, members: Address[]): string => {
|
||||||
|
if (!addressIds) return "";
|
||||||
|
addressIds = removeDuplicates(addressIds);
|
||||||
|
let res = "";
|
||||||
|
addressIds.forEach((addressId) => {
|
||||||
|
const address = members?.find((member) => member.id === parseInt(addressId));
|
||||||
|
if (address) res += address.email;
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
};
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, PropType } from "vue";
|
import { defineProps, PropType } from "vue";
|
||||||
import Badge from "@/components/Badge.vue";
|
import Badge from "@/components/Badge.vue";
|
||||||
|
import SvgLoader from "@/components/utils/SvgLoader.vue";
|
||||||
import { RoomType, Address, Room } from "@/store/models/model";
|
import { RoomType, Address, Room } from "@/store/models/model";
|
||||||
import MemberList from "./MemberList.vue";
|
import MemberList from "./MemberList.vue";
|
||||||
|
import imapAPI from "@/services/imapAPI";
|
||||||
|
|
||||||
const props = defineProps({ id: Number, room: Object as PropType<Room> });
|
const props = defineProps({ id: Number, room: Object as PropType<Room> });
|
||||||
|
|
||||||
@@ -14,21 +16,38 @@ const roomTitle = () => {
|
|||||||
return props.room?.roomName;
|
return props.room?.roomName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
console.log(props.room);
|
||||||
|
if (props.room?.id) {
|
||||||
|
imapAPI.deleteRoom(props.room.id);
|
||||||
|
}
|
||||||
|
// todo loading, delete
|
||||||
|
};
|
||||||
|
|
||||||
// todo remove us from list
|
// todo remove us from list
|
||||||
const to = () => props.room?.members.filter((member: Address) => member.type == "to");
|
const to = () => props.room?.members.filter((member: Address) => member.type == "to");
|
||||||
const cc = () => props.room?.members.filter((member: Address) => member.type == "cc");
|
const cc = () => props.room?.members.filter((member: Address) => member.type == "cc");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="main">
|
<div>
|
||||||
<div class="context">
|
<div class="context">
|
||||||
<div class="infos">
|
<div class="infos">
|
||||||
<Badge :value="RoomType[room?.roomType ?? 0]" />
|
<Badge :value="RoomType[room?.roomType ?? 0]" />
|
||||||
{{ roomTitle() }}
|
{{ roomTitle() }}
|
||||||
</div>
|
</div>
|
||||||
<div class="action">action: threads message important</div>
|
<div class="action">
|
||||||
|
<SvgLoader svg="list-unordered" classes="selectable" v-tooltip="{ text: 'Thread list' }" />
|
||||||
|
<SvgLoader svg="pushpin-line" classes="selectable" v-tooltip="{ text: 'Important messages' }" />
|
||||||
|
<SvgLoader
|
||||||
|
svg="delete-bin-4-line"
|
||||||
|
@click="handleDelete()"
|
||||||
|
classes="danger,selectable"
|
||||||
|
v-tooltip="{ text: 'Delete room' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="members" v-if="room?.roomType != RoomType.DM">
|
<div v-if="room?.roomType != RoomType.DM">
|
||||||
<MemberList class="members-list" v-if="to()?.length ?? 0 > 0" type="to" :members="to()" />
|
<MemberList class="members-list" v-if="to()?.length ?? 0 > 0" type="to" :members="to()" />
|
||||||
<MemberList class="members-list" v-if="cc()?.length ?? 0 > 0" type="cc" :members="cc()" />
|
<MemberList class="members-list" v-if="cc()?.length ?? 0 > 0" type="cc" :members="cc()" />
|
||||||
</div>
|
</div>
|
||||||
@@ -36,8 +55,6 @@ const cc = () => props.room?.members.filter((member: Address) => member.type ==
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main {
|
|
||||||
}
|
|
||||||
.context {
|
.context {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -52,9 +69,6 @@ const cc = () => props.room?.members.filter((member: Address) => member.type ==
|
|||||||
padding: 3px 5px;
|
padding: 3px 5px;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
.members {
|
|
||||||
}
|
|
||||||
|
|
||||||
.infos {
|
.infos {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Header from "./Header.vue";
|
|||||||
import Message from "../../components/structure/message/Message.vue";
|
import Message from "../../components/structure/message/Message.vue";
|
||||||
import MessageViewModal from "@/components/modals/MessageViewModal.vue";
|
import MessageViewModal from "@/components/modals/MessageViewModal.vue";
|
||||||
import Composer from "@/components/structure/message/Composer.vue";
|
import Composer from "@/components/structure/message/Composer.vue";
|
||||||
|
import ConfirmationModal from "@/components/modals/ConfirmationModal.vue";
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -17,10 +18,10 @@ const id = ref(parseInt(route.params.id));
|
|||||||
let room = ref();
|
let room = ref();
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
|
console.log(id.value);
|
||||||
store.commit("setActiveRoom", id.value);
|
store.commit("setActiveRoom", id.value);
|
||||||
room.value = store.getters.room(id.value);
|
room.value = store.getters.room(id.value);
|
||||||
messages.value = store.getters.messages(room.value?.id);
|
messages.value = store.getters.messages(id.value);
|
||||||
console.log(room.value);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeRouteUpdate(async (to, from) => {
|
onBeforeRouteUpdate(async (to, from) => {
|
||||||
@@ -28,9 +29,7 @@ onBeforeRouteUpdate(async (to, from) => {
|
|||||||
id.value = parseInt(to.params.id);
|
id.value = parseInt(to.params.id);
|
||||||
store.commit("setActiveRoom", id.value);
|
store.commit("setActiveRoom", id.value);
|
||||||
room.value = await store.getters.room(id.value);
|
room.value = await store.getters.room(id.value);
|
||||||
messages.value = store.getters.messages(room.value?.id);
|
messages.value = store.getters.messages(id.value);
|
||||||
|
|
||||||
console.log(room.value);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,7 +72,14 @@ provide("room", room);
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Composer v-if="shouldDisplayComposer() || true" />
|
<Composer v-if="shouldDisplayComposer() || true" />
|
||||||
<MessageViewModal :message="message" :messageId="messageIdView" @close="() => openMessageView(-1)" />
|
<MessageViewModal
|
||||||
|
:room="room"
|
||||||
|
:message="message"
|
||||||
|
:messageId="messageIdView"
|
||||||
|
@close="() => openMessageView(-1)"
|
||||||
|
/>
|
||||||
|
<!-- todo -->
|
||||||
|
<!-- <ConfirmationModal /> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,17 @@
|
|||||||
|
<script setup>
|
||||||
|
import Account from "./Account";
|
||||||
|
import AddAccountModal from "@/components/modals/AddAccountModal";
|
||||||
|
import store from "@/store/store";
|
||||||
|
import { onMounted } from "vue";
|
||||||
|
import { computed } from "@vue/reactivity";
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
store.dispatch("fetchAccounts");
|
||||||
|
});
|
||||||
|
|
||||||
|
const accounts = computed(() => store.state.accounts);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="main">
|
<div id="main">
|
||||||
<div id="userMenu">
|
<div id="userMenu">
|
||||||
@@ -6,32 +20,10 @@
|
|||||||
<span class="divider"></span>
|
<span class="divider"></span>
|
||||||
<Account v-for="(account, index) in accounts" :key="index" :data="account" />
|
<Account v-for="(account, index) in accounts" :key="index" :data="account" />
|
||||||
<span class="divider"></span>
|
<span class="divider"></span>
|
||||||
|
|
||||||
<AddAccountModal />
|
<AddAccountModal />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapState } from "vuex";
|
|
||||||
import Account from "./Account";
|
|
||||||
import AddAccountModal from "@/components/modals/AddAccountModal";
|
|
||||||
import store from "@/store/store";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "Accounts",
|
|
||||||
components: {
|
|
||||||
Account,
|
|
||||||
AddAccountModal,
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState(["accounts"]),
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
store.dispatch("fetchAccounts");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
#main {
|
#main {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user