@social-mail/social-mail-client 1.2.3 → 1.2.5

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.
Files changed (48) hide show
  1. package/dist/admin/AdminAppIndex.pack.js +803 -32
  2. package/dist/admin/AdminAppIndex.pack.js.map +1 -1
  3. package/dist/admin/AdminAppIndex.pack.min.js +150 -20
  4. package/dist/admin/AdminAppIndex.pack.min.js.map +1 -1
  5. package/dist/common/controls/audio/AudioRecorder.d.ts +10 -0
  6. package/dist/common/controls/audio/AudioRecorder.d.ts.map +1 -0
  7. package/dist/common/controls/audio/AudioRecorder.js +116 -0
  8. package/dist/common/controls/audio/AudioRecorder.js.map +1 -0
  9. package/dist/common/controls/audio/WebRTCService.d.ts +16 -0
  10. package/dist/common/controls/audio/WebRTCService.d.ts.map +1 -0
  11. package/dist/common/controls/audio/WebRTCService.js +61 -0
  12. package/dist/common/controls/audio/WebRTCService.js.map +1 -0
  13. package/dist/common/controls/buttons/RoundButton.d.ts +7 -0
  14. package/dist/common/controls/buttons/RoundButton.d.ts.map +1 -0
  15. package/dist/common/controls/buttons/RoundButton.js +51 -0
  16. package/dist/common/controls/buttons/RoundButton.js.map +1 -0
  17. package/dist/services/files/LocalFileService.d.ts +2 -0
  18. package/dist/services/files/LocalFileService.d.ts.map +1 -1
  19. package/dist/services/files/LocalFileService.js +32 -8
  20. package/dist/services/files/LocalFileService.js.map +1 -1
  21. package/dist/style/GlobalStyle.js +13 -12
  22. package/dist/style/GlobalStyle.js.map +1 -1
  23. package/dist/tsconfig.tsbuildinfo +1 -1
  24. package/dist/web/AppIndex.pack.js +803 -32
  25. package/dist/web/AppIndex.pack.js.map +1 -1
  26. package/dist/web/AppIndex.pack.min.js +150 -20
  27. package/dist/web/AppIndex.pack.min.js.map +1 -1
  28. package/dist/web/page/mails/ConversationPage.d.ts +1 -0
  29. package/dist/web/page/mails/ConversationPage.d.ts.map +1 -1
  30. package/dist/web/page/mails/ConversationPage.js +11 -1
  31. package/dist/web/page/mails/ConversationPage.js.map +1 -1
  32. package/dist/web/page/mails/MiniComposeBox.d.ts +10 -3
  33. package/dist/web/page/mails/MiniComposeBox.d.ts.map +1 -1
  34. package/dist/web/page/mails/MiniComposeBox.js +102 -7
  35. package/dist/web/page/mails/MiniComposeBox.js.map +1 -1
  36. package/dist/web/page/mails/service/ConversationService.d.ts +3 -1
  37. package/dist/web/page/mails/service/ConversationService.d.ts.map +1 -1
  38. package/dist/web/page/mails/service/ConversationService.js +62 -3
  39. package/dist/web/page/mails/service/ConversationService.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/common/controls/audio/AudioRecorder.tsx +102 -0
  42. package/src/common/controls/audio/WebRTCService.ts +61 -0
  43. package/src/common/controls/buttons/RoundButton.tsx +38 -0
  44. package/src/services/files/LocalFileService.tsx +35 -11
  45. package/src/style/GlobalStyle.ts +13 -12
  46. package/src/web/page/mails/ConversationPage.tsx +12 -1
  47. package/src/web/page/mails/MiniComposeBox.tsx +75 -6
  48. 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
+ }
@@ -31,7 +31,7 @@ export type UploadedFiles = CustomEvent<{ files: IUploadedFile[], extra: any}>;
31
31
 
32
32
  styled.css `
33
33
  display: inline-grid;
34
- grid-template-rows: auto auto;
34
+ grid-template-rows: auto auto auto;
35
35
 
36
36
  & > [data-element=file-name] {
37
37
  grid-row: 1;
@@ -40,15 +40,20 @@ export type UploadedFiles = CustomEvent<{ files: IUploadedFile[], extra: any}>;
40
40
  text-overflow: ellipsis;
41
41
  }
42
42
 
43
- & > [data-element=progress] {
43
+ & > progress {
44
44
  grid-row: 2;
45
45
  width: 100%;
46
46
  }
47
+ & > [data-element=error] {
48
+ grid-row: 3;
49
+ width: 100%;
50
+ }
47
51
 
48
52
  `.installGlobal("[data-attachment-file]");
49
53
 
50
54
  export interface IFileUpload {
51
55
  name: string;
56
+ error?: string;
52
57
  progress?: number;
53
58
  promise?: Promise<{ appFileID }>
54
59
  }
@@ -164,23 +169,38 @@ export default class LocalFileService extends TaskManager {
164
169
  const upload = {
165
170
  total: 0,
166
171
  tasks,
167
- progress:0
172
+ progress:0,
173
+ error: "",
168
174
  };
169
175
  for (const file of files) {
170
176
  upload.total += file.size;
171
177
  const uploadFile: IFileUpload = {
172
178
  name: file.name,
179
+ error: "",
173
180
  progress: 0
174
181
  };
175
182
  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;
183
+ let lastError: string;
184
+ for(let i=0; i<3; i++) {
185
+ try {
186
+ uploadFile.progress = 0.01;
187
+
188
+ // we will check if file exists and has same sha256?
189
+
190
+ const r = await this.uploadFile(file, extra, ct, (n) => uploadFile.progress = n);
191
+ upload.progress += r.file.size;
192
+ setTimeout(() => tasks.remove(uploadFile), 500);
193
+ upload.error = "";
194
+ lastError = null;
195
+ return r;
196
+ } catch (error) {
197
+ lastError = `${file.webkitRelativePath ?? ""}/${file.name} failed to upload.\n${error.stack ?? error}`;
198
+ uploadFile.error = lastError;
199
+ }
200
+ }
201
+ if (lastError) {
202
+ upload.error += "\n" + lastError;
203
+ }
184
204
  });
185
205
  tasks.push(uploadFile);
186
206
  }
@@ -231,6 +251,10 @@ class FileUploadWindow extends PopupWindow {
231
251
  min={0}
232
252
  max={1}
233
253
  value={Bind.source(file, (x) => x.source.progress)}/>
254
+ <div
255
+ data-element="error"
256
+ data-color="red"
257
+ text={Bind.source(file, (x) => x.source.error)}/>
234
258
  </div>}
235
259
  />
236
260
  </div>);
@@ -61,23 +61,24 @@ html, body {
61
61
  }
62
62
 
63
63
  * {
64
- scrollbar-width: 5px;
64
+ scrollbar-width: thin;
65
65
  scrollbar-color: var(--accent-color) white;
66
+ }
66
67
 
67
- &::-webkit-scrollbar {
68
- width: 5px;
69
- }
70
- &::-webkit-scrollbar-track {
68
+
69
+ ::-webkit-scrollbar {
70
+ width: 5px;
71
+ }
72
+ ::-webkit-scrollbar-track {
71
73
  background: white;
72
- }
73
- &::-webkit-scrollbar-thumb {
74
- background-color: white;
75
- border-radius: 20px;
76
- border: 3px solid var(--accent-color);
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.footerRenderer = () => <MiniComposeBox/>;
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 mailbox: IMailbox = null;
36
+ public mailboxContact: IMailboxContact = null;
29
37
 
30
- public message: string = "";
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.message)}/>
43
- <button
55
+ value={Bind.twoWaysImmediate(() => this.typedMessage)}/>
56
+ <i
44
57
  data-element="send"
45
- class="fas fa-send"/>
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);