Compare commits

..

10 Commits

Author SHA1 Message Date
grimhilt
edddf9afbf add link to the thread of a message 2023-07-15 00:19:39 +02:00
grimhilt
3bffd88108 add original message in thread 2023-07-14 19:05:37 +02:00
grimhilt
0094783a4e minor changes 2023-07-14 16:26:13 +02:00
grimhilt
f42d819e45 confirmation modal 2023-05-23 16:35:23 +02:00
grimhilt
467b0eebe9 setup accounts 2023-05-23 15:56:27 +02:00
grimhilt
e8e8555c2f add smtp port on front 2023-05-23 15:36:14 +02:00
grimhilt
af4cc2f6a0 create custom component 2023-05-23 15:19:52 +02:00
grimhilt
2cae8f12a7 add route room delete 2023-05-17 18:14:25 +02:00
grimhilt
7be2e84691 add actions header room 2023-05-17 18:00:15 +02:00
grimhilt
737a22e1f8 improve svgloader 2023-05-17 17:44:55 +02:00
30 changed files with 536 additions and 290 deletions

View File

@ -34,11 +34,11 @@ export default class MessageAbl {
} }
static async addFlag(body, res: Response) { static async addFlag(body, res: Response) {
await this.changeFlag(body, res, false); await MessageAbl.changeFlag(body, res, false);
} }
static async removeFlag(body, res: Response) { static async removeFlag(body, res: Response) {
await this.changeFlag(body, res, true); await MessageAbl.changeFlag(body, res, true);
} }
static async deleteRemoteUtil(message: Message, mailboxId: number, res, isFull: boolean): Promise<boolean> { static async deleteRemoteUtil(message: Message, mailboxId: number, res, isFull: boolean): Promise<boolean> {
@ -80,7 +80,7 @@ export default class MessageAbl {
static deleteRemoteOnly = async (body, res: Response) => { static deleteRemoteOnly = async (body, res: Response) => {
const { mailboxId, messageId } = body; const { mailboxId, messageId } = body;
const message = new Message().setMessageId(messageId); const message = new Message().setMessageId(messageId);
await this.deleteRemoteUtil(message, mailboxId, res, true); await MessageAbl.deleteRemoteUtil(message, mailboxId, res, true);
}; };
static deleteEverywhere = async (body, res: Response) => { static deleteEverywhere = async (body, res: Response) => {
@ -97,7 +97,7 @@ export default class MessageAbl {
// if message not deleted remotly, delete it // if message not deleted remotly, delete it
if (!message.isDeleted) { if (!message.isDeleted) {
const success = await this.deleteRemoteUtil(message, mailboxId, res, false); const success = await MessageAbl.deleteRemoteUtil(message, mailboxId, res, false);
if (!success) { if (!success) {
return; return;
} }
@ -106,7 +106,7 @@ export default class MessageAbl {
const room = await new Room().setRoomIdOnMessageId(messageId); const room = await new Room().setRoomIdOnMessageId(messageId);
try { try {
await message.delete(); await message.delete();
if (room.roomId && room.shouldDelete()) { if (room.roomId && await room.shouldDelete()) {
await room.delete(); await room.delete();
res.status(statusCode.OK).json({ deleteRoom: true }).send(); res.status(statusCode.OK).json({ deleteRoom: true }).send();
return; return;

View File

@ -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);
@ -27,7 +28,7 @@ export default class RoomAbl {
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 RoomAbl {
.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 RoomAbl {
}); });
} }
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
} }
} }

View File

@ -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
@ -104,9 +107,10 @@ export async function getMessages(roomId: number) {
field_name.field_id = header_field.field_id AND field_name.field_id = header_field.field_id AND
field_name.field_name = 'subject' field_name.field_name = 'subject'
) 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;

View File

@ -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];

View File

@ -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;

View File

@ -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);
}); });

View File

@ -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();
}
}
} }

View File

@ -23,6 +23,7 @@ export default class Room {
return this; return this;
} }
// check if the room have threads or messages
async shouldDelete(): Promise<boolean> { async shouldDelete(): Promise<boolean> {
if (!this._roomId) { if (!this._roomId) {
throw "shouldDelete needs to have a roomId set."; throw "shouldDelete needs to have a roomId set.";

View File

@ -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
} }

View File

@ -21,4 +21,8 @@ router.post("/response", async (req, res) => {
await validator.validate("response", req.body, res, RoomAbl.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;

View File

@ -0,0 +1,12 @@
{
"type": "object",
"properties": {
"roomId": {
"type": "number"
}
},
"required": [
"roomId"
],
"additionalProperties": false
}

View File

@ -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;

View File

@ -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);

View 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

View 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

View 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

View 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>

View 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>

View File

@ -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> <!-- <fieldset>
<legend>Authentification method</legend> <legend>Authentification method</legend>
<div class="field"> <div class="field">
<label>Plain password:</label> <Input label="Plain password:" v-model="pwd" type="password" />
<input v-model="pwd" type="password" />
</div> </div>
<div class="field"> <div class="field">
<label>Xoauth:</label> <Input label="Xoauth:" v-model="xoauth" type="text" />
<input v-model="xoauth" type="text" />
</div> </div>
<div class="field"> <div class="field">
<label>Xoauth2:</label> <Input label="Xoauth2:" v-model="xoauth2" type="text" />
<input v-model="xoauth2" type="text" />
</div> </div>
</fieldset> </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>

View 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>

View File

@ -33,6 +33,9 @@ onUnmounted(() => {
<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 +61,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;

View File

@ -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,25 +183,25 @@ 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 Left', 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 Left', 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: 'Align Left', 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;

View File

@ -7,6 +7,7 @@ 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";
export interface Props { export interface Props {
msg: Message; msg: Message;
@ -43,6 +44,8 @@ const classes = (): string => {
} }
return "msg-basic"; return "msg-basic";
}; };
const router = useRouter();
</script> </script>
<!-- to if to is more than me <!-- to if to is more than me
cc --> cc -->
@ -81,16 +84,34 @@ 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" />
<!-- <SvgLoader svg="expand-left-fill" :loading="true" /> -->
<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;

View File

@ -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>

View File

@ -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);

View File

@ -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 });
},
}; };

View File

@ -15,6 +15,7 @@ export interface Message {
content: string; content: string;
date: string; date: string;
flags: string[]; flags: string[];
threads: number | null;
} }
export enum LoadingState { export enum LoadingState {

View File

@ -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;
} }

View File

@ -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();
@ -72,6 +73,8 @@ provide("room", room);
</div> </div>
<Composer v-if="shouldDisplayComposer() || true" /> <Composer v-if="shouldDisplayComposer() || true" />
<MessageViewModal :message="message" :messageId="messageIdView" @close="() => openMessageView(-1)" /> <MessageViewModal :message="message" :messageId="messageIdView" @close="() => openMessageView(-1)" />
<!-- todo -->
<!-- <ConfirmationModal /> -->
</div> </div>
</template> </template>

View File

@ -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;