@social-mail/social-mail-client 1.2.2 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/AdminAppIndex.pack.js +819 -51
- package/dist/admin/AdminAppIndex.pack.js.map +1 -1
- package/dist/admin/AdminAppIndex.pack.min.js +145 -31
- package/dist/admin/AdminAppIndex.pack.min.js.map +1 -1
- package/dist/common/controls/audio/AudioRecorder.d.ts +10 -0
- package/dist/common/controls/audio/AudioRecorder.d.ts.map +1 -0
- package/dist/common/controls/audio/AudioRecorder.js +116 -0
- package/dist/common/controls/audio/AudioRecorder.js.map +1 -0
- package/dist/common/controls/audio/WebRTCService.d.ts +16 -0
- package/dist/common/controls/audio/WebRTCService.d.ts.map +1 -0
- package/dist/common/controls/audio/WebRTCService.js +61 -0
- package/dist/common/controls/audio/WebRTCService.js.map +1 -0
- package/dist/common/controls/buttons/RoundButton.d.ts +7 -0
- package/dist/common/controls/buttons/RoundButton.d.ts.map +1 -0
- package/dist/common/controls/buttons/RoundButton.js +51 -0
- package/dist/common/controls/buttons/RoundButton.js.map +1 -0
- package/dist/common/controls/html/InlineHtmlEditorWithToolbar.js.map +1 -1
- package/dist/services/files/LocalFileService.d.ts +7 -2
- package/dist/services/files/LocalFileService.d.ts.map +1 -1
- package/dist/services/files/LocalFileService.js +48 -27
- package/dist/services/files/LocalFileService.js.map +1 -1
- package/dist/style/GlobalStyle.js +13 -12
- package/dist/style/GlobalStyle.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/web/AppIndex.pack.js +819 -51
- package/dist/web/AppIndex.pack.js.map +1 -1
- package/dist/web/AppIndex.pack.min.js +145 -31
- package/dist/web/AppIndex.pack.min.js.map +1 -1
- package/dist/web/page/mails/ConversationPage.d.ts +1 -0
- package/dist/web/page/mails/ConversationPage.d.ts.map +1 -1
- package/dist/web/page/mails/ConversationPage.js +11 -1
- package/dist/web/page/mails/ConversationPage.js.map +1 -1
- package/dist/web/page/mails/MiniComposeBox.d.ts +10 -3
- package/dist/web/page/mails/MiniComposeBox.d.ts.map +1 -1
- package/dist/web/page/mails/MiniComposeBox.js +102 -7
- package/dist/web/page/mails/MiniComposeBox.js.map +1 -1
- package/dist/web/page/mails/service/ConversationService.d.ts +3 -1
- package/dist/web/page/mails/service/ConversationService.d.ts.map +1 -1
- package/dist/web/page/mails/service/ConversationService.js +62 -3
- package/dist/web/page/mails/service/ConversationService.js.map +1 -1
- package/package.json +1 -1
- package/src/common/controls/audio/AudioRecorder.tsx +102 -0
- package/src/common/controls/audio/WebRTCService.ts +61 -0
- package/src/common/controls/buttons/RoundButton.tsx +38 -0
- package/src/common/controls/html/InlineHtmlEditorWithToolbar.tsx +1 -1
- package/src/services/files/LocalFileService.tsx +44 -22
- package/src/style/GlobalStyle.ts +13 -12
- package/src/web/page/mails/ConversationPage.tsx +12 -1
- package/src/web/page/mails/MiniComposeBox.tsx +75 -6
- package/src/web/page/mails/service/ConversationService.tsx +53 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { CancelToken } from "@web-atoms/core/dist/core/types";
|
|
2
|
+
import DISingleton from "@web-atoms/core/dist/di/DISingleton";
|
|
3
|
+
import TimeSpan from "@web-atoms/date-time/dist/TimeSpan";
|
|
4
|
+
|
|
5
|
+
export interface IRecordingOptions {
|
|
6
|
+
progress: (time: TimeSpan) => void;
|
|
7
|
+
segments?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class WebRecordingSession {
|
|
11
|
+
|
|
12
|
+
public duration: TimeSpan;
|
|
13
|
+
|
|
14
|
+
private recorder: MediaRecorder;
|
|
15
|
+
|
|
16
|
+
constructor(stream: MediaStream) {
|
|
17
|
+
this.duration = TimeSpan.fromSeconds(0);
|
|
18
|
+
this.recorder = new MediaRecorder(stream);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public start(segments?: number): Promise<Blob> {
|
|
22
|
+
const buffer = [];
|
|
23
|
+
return new Promise<Blob>((resolve, reject) => {
|
|
24
|
+
this.recorder.ondataavailable = (be) => {
|
|
25
|
+
buffer.push(be.data);
|
|
26
|
+
};
|
|
27
|
+
const start = Date.now();
|
|
28
|
+
const id = setInterval(() => {
|
|
29
|
+
const seconds = Math.ceil((Date.now() - start) / 1000);
|
|
30
|
+
this.duration = TimeSpan.fromSeconds(seconds);
|
|
31
|
+
}, 1000);
|
|
32
|
+
this.recorder.onerror = (me: ErrorEvent) => {
|
|
33
|
+
clearInterval(id);
|
|
34
|
+
reject(me.error);
|
|
35
|
+
};
|
|
36
|
+
this.recorder.onstop = () => {
|
|
37
|
+
clearInterval(id);
|
|
38
|
+
resolve(new Blob(buffer, { type: this.recorder.mimeType}));
|
|
39
|
+
};
|
|
40
|
+
this.recorder.start(segments);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public stop() {
|
|
45
|
+
this.recorder.stop();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default class WebRTCService {
|
|
50
|
+
|
|
51
|
+
public static async startAudioSession() {
|
|
52
|
+
return new WebRecordingSession(await navigator.mediaDevices.getUserMedia({ audio: true }));
|
|
53
|
+
// return new Promise<WebRecordingSession>((resolve, reject) => {
|
|
54
|
+
// navigator.mediaDevices.getUserMedia({
|
|
55
|
+
// audio: true
|
|
56
|
+
// }, (stream) => resolve(new WebRecordingSession(stream)),
|
|
57
|
+
// (error) => reject(new Error(`${error.message}`)));
|
|
58
|
+
// });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import XNode from "@web-atoms/core/dist/core/XNode";
|
|
2
|
+
import styled from "@web-atoms/core/dist/style/styled";
|
|
3
|
+
import IElement from "@web-atoms/web-controls/dist/basic/IElement";
|
|
4
|
+
|
|
5
|
+
styled.css `
|
|
6
|
+
border-radius: 9999px;
|
|
7
|
+
height: 70px;
|
|
8
|
+
width: 70px;
|
|
9
|
+
min-height: 70px;
|
|
10
|
+
text-align: center;
|
|
11
|
+
align-self: center;
|
|
12
|
+
border: none;
|
|
13
|
+
background-color: var(--accent-color);
|
|
14
|
+
color: var(--accent-text-color);
|
|
15
|
+
font-weight: bold;
|
|
16
|
+
flex-basis: auto;
|
|
17
|
+
margin-left: auto;
|
|
18
|
+
margin-right: auto;
|
|
19
|
+
`.installGlobal("button[data-round-button=round-button]");
|
|
20
|
+
|
|
21
|
+
export interface IRoundButton extends IElement {
|
|
22
|
+
icon?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function RoundButton({
|
|
26
|
+
icon,
|
|
27
|
+
... a
|
|
28
|
+
}: IRoundButton, ... nodes: XNode[]) {
|
|
29
|
+
if (icon) {
|
|
30
|
+
return <button data-color="white" data-round-button="round-button" {... a} >
|
|
31
|
+
<i class={`${icon} white`}/>
|
|
32
|
+
{ ... nodes}
|
|
33
|
+
</button>;
|
|
34
|
+
}
|
|
35
|
+
return <button data-color="white" data-round-button="round-button" {... a} >
|
|
36
|
+
{ ... nodes}
|
|
37
|
+
</button>;
|
|
38
|
+
}
|
|
@@ -58,7 +58,7 @@ const onAttachInlineImage = async (ce: CustomEvent) => {
|
|
|
58
58
|
reader.onload = () => {
|
|
59
59
|
c.executeCommand("insertHTML", void 0, `<div><img
|
|
60
60
|
data-content="data-url"
|
|
61
|
-
src="${reader.result}" style='max-height: 700px;'/>
|
|
61
|
+
src="${reader.result as any}" style='max-height: 700px;'/>
|
|
62
62
|
</div>`);
|
|
63
63
|
};
|
|
64
64
|
reader.readAsDataURL(file);
|
|
@@ -10,6 +10,8 @@ import XNode from "@web-atoms/core/dist/core/XNode";
|
|
|
10
10
|
import AtomRepeater from "@web-atoms/web-controls/dist/basic/AtomRepeater";
|
|
11
11
|
import Bind from "@web-atoms/core/dist/core/Bind";
|
|
12
12
|
import PopupService from "@web-atoms/core/dist/web/services/PopupService";
|
|
13
|
+
import { IAppFile } from "../../model/model";
|
|
14
|
+
import TaskManager from "@web-atoms/entity/dist/models/TaskManager";
|
|
13
15
|
|
|
14
16
|
export const acceptDocumentAndMedia = ".doc, .docx, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, video/*, application/pdf, .pdf, .txt, audio/*, image/*, .csv, text/csv, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, text/plain, text/html";
|
|
15
17
|
|
|
@@ -30,16 +32,9 @@ export type UploadedFiles = CustomEvent<{ files: IUploadedFile[], extra: any}>;
|
|
|
30
32
|
styled.css `
|
|
31
33
|
display: inline-grid;
|
|
32
34
|
grid-template-rows: auto auto;
|
|
33
|
-
grid-template-columns: auto 1fr auto;
|
|
34
|
-
|
|
35
|
-
& > [data-element=preview] {
|
|
36
|
-
grid-row: 1 / span 2;
|
|
37
|
-
grid-column: 1;
|
|
38
|
-
}
|
|
39
35
|
|
|
40
36
|
& > [data-element=file-name] {
|
|
41
37
|
grid-row: 1;
|
|
42
|
-
grid-column: 2;
|
|
43
38
|
overflow: hidden;
|
|
44
39
|
white-space: nowrap;
|
|
45
40
|
text-overflow: ellipsis;
|
|
@@ -47,14 +42,9 @@ export type UploadedFiles = CustomEvent<{ files: IUploadedFile[], extra: any}>;
|
|
|
47
42
|
|
|
48
43
|
& > [data-element=progress] {
|
|
49
44
|
grid-row: 2;
|
|
50
|
-
grid-column: 2 / span 2;
|
|
51
45
|
width: 100%;
|
|
52
46
|
}
|
|
53
47
|
|
|
54
|
-
& > [data-element=delete] {
|
|
55
|
-
grid-row: 1;
|
|
56
|
-
grid-column: 3;
|
|
57
|
-
}
|
|
58
48
|
`.installGlobal("[data-attachment-file]");
|
|
59
49
|
|
|
60
50
|
export interface IFileUpload {
|
|
@@ -64,12 +54,13 @@ export interface IFileUpload {
|
|
|
64
54
|
}
|
|
65
55
|
|
|
66
56
|
@DISingleton()
|
|
67
|
-
export default class LocalFileService {
|
|
57
|
+
export default class LocalFileService extends TaskManager {
|
|
68
58
|
|
|
69
59
|
@Inject
|
|
70
60
|
private app: App;
|
|
71
61
|
|
|
72
62
|
constructor() {
|
|
63
|
+
super();
|
|
73
64
|
document.body.addEventListener("uploadRequested", (ce: CustomEvent) => {
|
|
74
65
|
const { target, detail: { files, extra, uploadEvent } } = ce;
|
|
75
66
|
ce.preventDefault();
|
|
@@ -165,14 +156,35 @@ export default class LocalFileService {
|
|
|
165
156
|
});
|
|
166
157
|
}
|
|
167
158
|
|
|
168
|
-
|
|
169
|
-
|
|
159
|
+
upload(files: File[], extra: any, ct?: CancelToken) {
|
|
160
|
+
|
|
161
|
+
const { parentID } = extra;
|
|
162
|
+
|
|
163
|
+
const tasks = [] as IFileUpload[];
|
|
164
|
+
const upload = {
|
|
165
|
+
total: 0,
|
|
166
|
+
tasks,
|
|
167
|
+
progress:0
|
|
168
|
+
};
|
|
169
|
+
for (const file of files) {
|
|
170
|
+
upload.total += file.size;
|
|
170
171
|
const uploadFile: IFileUpload = {
|
|
171
|
-
name: file.name
|
|
172
|
+
name: file.name,
|
|
173
|
+
progress: 0
|
|
172
174
|
};
|
|
173
|
-
uploadFile.promise = this.
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
uploadFile.promise = this.run(async () => {
|
|
176
|
+
uploadFile.progress = 0.01;
|
|
177
|
+
|
|
178
|
+
// we will check if file exists and has same sha256?
|
|
179
|
+
|
|
180
|
+
const r = await this.uploadFile(file, extra, ct, (n) => uploadFile.progress = n);
|
|
181
|
+
upload.progress += r.file.size;
|
|
182
|
+
setTimeout(() => tasks.remove(uploadFile), 500);
|
|
183
|
+
return r;
|
|
184
|
+
});
|
|
185
|
+
tasks.push(uploadFile);
|
|
186
|
+
}
|
|
187
|
+
return upload;
|
|
176
188
|
}
|
|
177
189
|
|
|
178
190
|
}
|
|
@@ -197,17 +209,27 @@ class FileUploadWindow extends PopupWindow {
|
|
|
197
209
|
await this.requestCancel();
|
|
198
210
|
}, null, { capture: true });
|
|
199
211
|
|
|
200
|
-
|
|
212
|
+
const task = this.localFileService.upload(this.parameters.files, this.parameters.extra, this.cancelToken);
|
|
213
|
+
this.files = task.tasks;
|
|
214
|
+
|
|
215
|
+
this.footerRenderer = () => <div data-layout="command-row">
|
|
216
|
+
<progress min={0} max={task.total} value={Bind.source(task, (x) => x.source.progress)}/>
|
|
217
|
+
</div>;
|
|
201
218
|
|
|
202
|
-
this.render(<div
|
|
219
|
+
this.render(<div
|
|
220
|
+
data-overflow="auto"
|
|
221
|
+
style-max-height="500px"
|
|
222
|
+
style-max-width="400px">
|
|
203
223
|
<AtomRepeater
|
|
224
|
+
data-layout="vertical-flex-stretch-items"
|
|
204
225
|
items={this.files}
|
|
205
226
|
itemRenderer={(file) =>
|
|
206
227
|
<div data-attachment-file="file">
|
|
207
228
|
<div data-element="file-name" text={file.name}/>
|
|
208
229
|
<progress
|
|
230
|
+
style-display={Bind.source(file, (x) => x.source.progress > 0)}
|
|
209
231
|
min={0}
|
|
210
|
-
max={
|
|
232
|
+
max={1}
|
|
211
233
|
value={Bind.source(file, (x) => x.source.progress)}/>
|
|
212
234
|
</div>}
|
|
213
235
|
/>
|
package/src/style/GlobalStyle.ts
CHANGED
|
@@ -61,23 +61,24 @@ html, body {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
* {
|
|
64
|
-
scrollbar-width:
|
|
64
|
+
scrollbar-width: thin;
|
|
65
65
|
scrollbar-color: var(--accent-color) white;
|
|
66
|
+
}
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
|
|
69
|
+
::-webkit-scrollbar {
|
|
70
|
+
width: 5px;
|
|
71
|
+
}
|
|
72
|
+
::-webkit-scrollbar-track {
|
|
71
73
|
background: white;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
74
|
+
border-radius: 20px;
|
|
75
|
+
}
|
|
76
|
+
::-webkit-scrollbar-thumb {
|
|
77
|
+
background: var(--accent-color);
|
|
78
|
+
border-radius: 20px;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
body {
|
|
81
|
+
body {
|
|
81
82
|
font-family: trebuchet ms, "Source Sans Pro";
|
|
82
83
|
font-size: 16px;
|
|
83
84
|
justify-content: stretch;
|
|
@@ -81,6 +81,8 @@ export default class ConversationPage extends ContentPage {
|
|
|
81
81
|
|
|
82
82
|
private mails: IMailboxContactEmail[] = [];
|
|
83
83
|
|
|
84
|
+
private draft: IEmail = null;
|
|
85
|
+
|
|
84
86
|
private search: string = "";
|
|
85
87
|
private version = 1;
|
|
86
88
|
|
|
@@ -191,7 +193,11 @@ export default class ConversationPage extends ContentPage {
|
|
|
191
193
|
</div>
|
|
192
194
|
</PopupActionButton>;
|
|
193
195
|
|
|
194
|
-
this.
|
|
196
|
+
this.draft = await this.conversationService.getDraft(this.parameters);
|
|
197
|
+
|
|
198
|
+
this.footerRenderer = () => <MiniComposeBox
|
|
199
|
+
mailboxContact={this.parameters}
|
|
200
|
+
draft={this.draft} />;
|
|
195
201
|
}
|
|
196
202
|
|
|
197
203
|
@Action({ onEvent: "click"})
|
|
@@ -256,6 +262,11 @@ export default class ConversationPage extends ContentPage {
|
|
|
256
262
|
icon="fa-regular fa-clock-rotate-left"
|
|
257
263
|
label="Logs"
|
|
258
264
|
/>
|
|
265
|
+
{ thread.email?.status === "draft" && <MenuItem
|
|
266
|
+
data-click-event="delete-draft"
|
|
267
|
+
icon="fa-regular fa-clock-rotate-left"
|
|
268
|
+
label="Delete Draft"
|
|
269
|
+
/>}
|
|
259
270
|
</div>);
|
|
260
271
|
}
|
|
261
272
|
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import XNode from "@web-atoms/core/dist/core/XNode";
|
|
2
2
|
import styled from "@web-atoms/core/dist/style/styled";
|
|
3
3
|
import { AtomControl } from "@web-atoms/core/dist/web/controls/AtomControl";
|
|
4
|
-
import { IMailbox } from "../../../model/model";
|
|
4
|
+
import { AppFile, Email, EmailRecipient, IAppFile, IEmail, IMailbox, IMailboxContact, MailboxEmail } from "../../../model/model";
|
|
5
5
|
import Bind from "@web-atoms/core/dist/core/Bind";
|
|
6
|
+
import Action from "@web-atoms/core/dist/view-model/Action";
|
|
7
|
+
import AudioRecorder from "../../../common/controls/audio/AudioRecorder";
|
|
8
|
+
import { Inject } from "@web-atoms/core/dist/di/Inject";
|
|
9
|
+
import ConversationService from "./service/ConversationService";
|
|
10
|
+
import { IUploadedFile } from "../../../services/files/LocalFileService";
|
|
11
|
+
import AtomRepeater from "@web-atoms/web-controls/dist/basic/AtomRepeater";
|
|
12
|
+
import InjectProperty from "@web-atoms/core/dist/core/InjectProperty";
|
|
13
|
+
import { AtomBinder } from "@web-atoms/core/dist/core/AtomBinder";
|
|
6
14
|
|
|
7
15
|
const css = styled.css `
|
|
8
16
|
display: grid;
|
|
@@ -25,9 +33,14 @@ const css = styled.css `
|
|
|
25
33
|
|
|
26
34
|
export default class MiniComposeBox extends AtomControl {
|
|
27
35
|
|
|
28
|
-
public
|
|
36
|
+
public mailboxContact: IMailboxContact = null;
|
|
29
37
|
|
|
30
|
-
public
|
|
38
|
+
public draft: IEmail = null;
|
|
39
|
+
|
|
40
|
+
public typedMessage: string = "";
|
|
41
|
+
|
|
42
|
+
@InjectProperty
|
|
43
|
+
private conversationService: ConversationService;
|
|
31
44
|
|
|
32
45
|
constructor(app, e) {
|
|
33
46
|
super(app, e);
|
|
@@ -39,11 +52,67 @@ export default class MiniComposeBox extends AtomControl {
|
|
|
39
52
|
this.render(<div>
|
|
40
53
|
<textarea
|
|
41
54
|
data-element="editor"
|
|
42
|
-
value={Bind.twoWaysImmediate(() => this.
|
|
43
|
-
<
|
|
55
|
+
value={Bind.twoWaysImmediate(() => this.typedMessage)}/>
|
|
56
|
+
<i
|
|
44
57
|
data-element="send"
|
|
45
|
-
class="
|
|
58
|
+
class="fa-solid fa-microphone-lines mic"
|
|
59
|
+
event-click={() => this.sendAudioMessage()}
|
|
60
|
+
style-display={Bind.oneWay(() => !this.typedMessage)} />
|
|
61
|
+
<i
|
|
62
|
+
data-element="send"
|
|
63
|
+
class="fa-solid fa-angle-double-right send"
|
|
64
|
+
style-display={Bind.oneWay(() => !!this.typedMessage)}
|
|
65
|
+
event-click={() => this.sendMessage()}
|
|
66
|
+
/>
|
|
67
|
+
<AtomRepeater
|
|
68
|
+
items={Bind.oneWay(() => this.draft?.attachments)}
|
|
69
|
+
data-layout="flex-wrap-padding"
|
|
70
|
+
itemRenderer={(item: IAppFile) => <div data-layout="row"><label text={item.name}/></div>}
|
|
71
|
+
/>
|
|
46
72
|
</div>);
|
|
47
73
|
}
|
|
48
74
|
|
|
75
|
+
@Action({ onEvent: "input", defer: 500, blockMultipleExecution: true })
|
|
76
|
+
async onChanged() {
|
|
77
|
+
if (!this.typedMessage) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.draft ??= Email.create({
|
|
81
|
+
attachments: []
|
|
82
|
+
});
|
|
83
|
+
this.draft.mdBody = this.typedMessage;
|
|
84
|
+
await this.save();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
sendMessage(): any {
|
|
88
|
+
throw new Error("Method not implemented.");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Action({})
|
|
92
|
+
private async sendAudioMessage() {
|
|
93
|
+
const uploadedFile = await AudioRecorder.show<IUploadedFile>();
|
|
94
|
+
this.draft ??= Email.create({
|
|
95
|
+
attachments: []
|
|
96
|
+
});
|
|
97
|
+
this.draft.mdBody = this.typedMessage;
|
|
98
|
+
this.addFile(uploadedFile);
|
|
99
|
+
await this.save();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private addFile({ appFileID, url, file }: IUploadedFile) {
|
|
103
|
+
this.draft.attachments ??= [];
|
|
104
|
+
AtomBinder.refreshValue(this, "draft");
|
|
105
|
+
this.draft.attachments.add(AppFile.create({
|
|
106
|
+
appFileID,
|
|
107
|
+
contentType: file.type,
|
|
108
|
+
name: file.name,
|
|
109
|
+
contentID: appFileID.toString(36),
|
|
110
|
+
url
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private async save() {
|
|
115
|
+
this.draft = await this.conversationService.draft({ ... this.mailboxContact, email: this.draft }, true);
|
|
116
|
+
}
|
|
117
|
+
|
|
49
118
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import DISingleton from "@web-atoms/core/dist/di/DISingleton";
|
|
3
3
|
import { Inject } from "@web-atoms/core/dist/di/Inject";
|
|
4
4
|
import EntityService from "../../../../services/EntityService";
|
|
5
|
-
import { Email, EmailEngagement, IEmail, IMailboxContact, IMailboxContactEmail, MailboxContactEmail } from "../../../../model/model";
|
|
5
|
+
import { Email, EmailEngagement, EmailRecipient, IEmail, IMailboxContact, IMailboxContactEmail, MailboxContactEmail, MailboxEmail } from "../../../../model/model";
|
|
6
6
|
import { Sql } from "../../../../common/Sql";
|
|
7
7
|
import DomToString from "@web-atoms/email-dom/dist/DomToString";
|
|
8
8
|
import XNode from "@web-atoms/core/dist/core/XNode";
|
|
@@ -102,6 +102,56 @@ export default class ConversationService {
|
|
|
102
102
|
@Inject
|
|
103
103
|
private entityService: EntityService;
|
|
104
104
|
|
|
105
|
+
public async getDraft({ mailboxID, emailAddressID, email }: IMailboxContactEmail) {
|
|
106
|
+
const draft = "draft";
|
|
107
|
+
return await this.entityService.query(MailboxContactEmail)
|
|
108
|
+
.where({ mailboxID, emailAddressID, draft }, (p) =>
|
|
109
|
+
(x) => x.mailboxID === p.mailboxID
|
|
110
|
+
&& x.emailAddressID === p.emailAddressID
|
|
111
|
+
&& x.email.status === p.draft)
|
|
112
|
+
.orderByDescending((x) => x.emailID)
|
|
113
|
+
.include((x) => [x.email.recipients, x.email.attachments])
|
|
114
|
+
.firstOrDefault();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public async draft({ mailboxID, emailAddressID, email }: IMailboxContactEmail, create = false) {
|
|
118
|
+
if (email?.emailID) {
|
|
119
|
+
// save..
|
|
120
|
+
if (!email.textBody) {
|
|
121
|
+
email.textBody = email.mdBody;
|
|
122
|
+
}
|
|
123
|
+
if (!email.subject) {
|
|
124
|
+
email.subject = email.textBody.substring(0, 80);
|
|
125
|
+
}
|
|
126
|
+
const cloner = this.entityService.cloner(email)
|
|
127
|
+
.include((x) => x.attachments)
|
|
128
|
+
.include((x) => x.recipients);
|
|
129
|
+
await this.entityService.save(cloner.copy);
|
|
130
|
+
return email;
|
|
131
|
+
}
|
|
132
|
+
if (create && email) {
|
|
133
|
+
email.recipients ??= [];
|
|
134
|
+
email.mailboxes ??= [];
|
|
135
|
+
if (!email.mailboxes.some((m) => m.mailboxID === mailboxID)) {
|
|
136
|
+
email.mailboxes.push(MailboxEmail.create({
|
|
137
|
+
mailboxID
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
if(!email.recipients.some((r) => r.emailAddressID === emailAddressID)) {
|
|
141
|
+
email.recipients.push(EmailRecipient.create({
|
|
142
|
+
emailAddressID
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
const mce = MailboxContactEmail.create({
|
|
146
|
+
mailboxID,
|
|
147
|
+
emailAddressID,
|
|
148
|
+
email
|
|
149
|
+
});
|
|
150
|
+
await this.entityService.save(mce);
|
|
151
|
+
}
|
|
152
|
+
return email;
|
|
153
|
+
}
|
|
154
|
+
|
|
105
155
|
public async list({
|
|
106
156
|
search = "",
|
|
107
157
|
mailboxID,
|
|
@@ -143,6 +193,8 @@ export default class ConversationService {
|
|
|
143
193
|
// }
|
|
144
194
|
|
|
145
195
|
// load recent engagements
|
|
196
|
+
email.htmlBody ??= "";
|
|
197
|
+
email.textBody ??= "";
|
|
146
198
|
tasks.push(downloadEmailContent(this.entityService, email));
|
|
147
199
|
}
|
|
148
200
|
await Promise.all(tasks);
|