Compare commits

...

5 Commits

Author SHA1 Message Date
grimhilt
adb7a1161a add implementation of colors for address in front 2023-07-18 16:16:40 +02:00
grimhilt
3c069ed2f8 better title in message view modal 2023-07-16 16:46:38 +02:00
grimhilt
ae73326820 improve add account flow 2023-07-16 16:24:30 +02:00
grimhilt
f40b6758de add members in message modal 2023-07-16 15:54:57 +02:00
grimhilt
43e8cc7d3e correct typo 2023-07-15 00:27:58 +02:00
16 changed files with 121 additions and 52 deletions

View File

@ -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(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -142,11 +142,11 @@ function mailChange() {
/> />
</div> </div>
<!-- <fieldset>
<legend>Authentification method</legend>
<div class="field"> <div class="field">
<Input label="Plain password:" v-model="pwd" type="password" /> <Input label="Plain password:" v-model="pwd" type="password" />
</div> </div>
<!-- <fieldset>
<legend>Authentification method</legend>
<div class="field"> <div class="field">
<Input label="Xoauth:" v-model="xoauth" type="text" /> <Input label="Xoauth:" v-model="xoauth" type="text" />
</div> </div>

View File

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

View File

@ -26,7 +26,9 @@ 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">
<slot name="header">
<h2>{{ props.title }}</h2> <h2>{{ props.title }}</h2>
</slot>
<div class="close-button" @click="close"></div> <div class="close-button" @click="close"></div>
</header> </header>

View File

@ -190,19 +190,19 @@ const getClasses = (isActive, disabled = false) => {
svg="align-center" svg="align-center"
@click="editor.chain().focus().setTextAlign('center').run()" @click="editor.chain().focus().setTextAlign('center').run()"
:classes="getClasses(editor.isActive({ textAlign: 'center' }))" :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()"
:classes="getClasses(editor.isActive({ textAlign: 'right' }))" :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()"
:classes="getClasses(editor.isActive({ textAlign: 'justify' }))" :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>

View File

@ -1,13 +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 { useRouter } from "vue-router";
import { displayAddresses } from "@/utils/address";
export interface Props { export interface Props {
msg: Message; msg: Message;
@ -21,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;
@ -45,6 +34,15 @@ 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(); const router = useRouter();
</script> </script>
<!-- to if to is more than me <!-- to if to is more than me
@ -54,9 +52,9 @@ const router = useRouter();
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">
@ -73,7 +71,7 @@ const router = useRouter();
}} }}
</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"
@ -86,7 +84,6 @@ const router = useRouter();
</div> </div>
<div id="thread-link" v-if="props.msg?.thread" @click="router.push(`/${props.msg?.thread}`)"> <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" />
<!-- <SvgLoader svg="expand-left-fill" :loading="true" /> -->
<span>Go to the full conversation.</span> <span>Go to the full conversation.</span>
</div> </div>
</div> </div>
@ -133,8 +130,11 @@ const router = useRouter();
.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 {

View File

@ -15,7 +15,7 @@ export interface Message {
content: string; content: string;
date: string; date: string;
flags: string[]; flags: string[];
threads: number | null; thread: number | null;
} }
export enum LoadingState { export enum LoadingState {
@ -47,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;
} }

View File

@ -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;
@ -154,6 +155,10 @@ const store = createStore<State>({
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);
}); });
}, },

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

View File

@ -72,7 +72,12 @@ 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 --> <!-- todo -->
<!-- <ConfirmationModal /> --> <!-- <ConfirmationModal /> -->
</div> </div>