@bobfrankston/mailx 1.0.465 → 1.0.500
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/.globalize.json5 +25 -0
- package/README.md +17 -420
- package/bin/mailx.js +87 -84
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +87 -84
- package/client/android.html +5 -5
- package/client/app.js +42 -38
- package/client/components/folder-tree.js +7 -5
- package/client/components/message-list.js +485 -448
- package/client/components/message-viewer.js +36 -41
- package/client/index.html +8 -8
- package/client/lib/message-state.js +46 -65
- package/index.js +59 -0
- package/package.json +12 -114
- package/packages/mailx-send/mailsend/{package-lock.json → node_modules/.package-lock.json} +0 -16
- package/packages/mailx-send/mailsend/node_modules/@types/node/LICENSE +21 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/README.md +15 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/assert/strict.d.ts +111 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/assert.d.ts +1078 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/async_hooks.d.ts +603 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/buffer.buffer.d.ts +472 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/buffer.d.ts +1934 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/child_process.d.ts +1476 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/cluster.d.ts +578 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/compatibility/disposable.d.ts +14 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/compatibility/index.d.ts +9 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/compatibility/indexable.d.ts +20 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/compatibility/iterators.d.ts +20 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/console.d.ts +452 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/constants.d.ts +21 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/crypto.d.ts +4545 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/dgram.d.ts +600 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/diagnostics_channel.d.ts +578 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/dns/promises.d.ts +503 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/dns.d.ts +923 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/domain.d.ts +170 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/events.d.ts +976 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/fs/promises.d.ts +1295 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/fs.d.ts +4461 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/globals.d.ts +172 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/globals.typedarray.d.ts +38 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/http.d.ts +2089 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/http2.d.ts +2644 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/https.d.ts +579 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/index.d.ts +97 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/inspector.d.ts +253 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/inspector.generated.d.ts +4052 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/module.d.ts +891 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/net.d.ts +1057 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/os.d.ts +506 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/package.json +145 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/path.d.ts +200 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/perf_hooks.d.ts +968 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/process.d.ts +2084 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/punycode.d.ts +117 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/querystring.d.ts +152 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/readline/promises.d.ts +161 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/readline.d.ts +594 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/repl.d.ts +428 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/sea.d.ts +153 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/sqlite.d.ts +721 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/stream/consumers.d.ts +38 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/stream/promises.d.ts +90 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/stream/web.d.ts +622 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/stream.d.ts +1664 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/string_decoder.d.ts +67 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/test.d.ts +2163 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/timers/promises.d.ts +108 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/timers.d.ts +287 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/tls.d.ts +1319 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/trace_events.d.ts +197 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/ts5.6/buffer.buffer.d.ts +468 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/ts5.6/globals.typedarray.d.ts +34 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/ts5.6/index.d.ts +97 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/tty.d.ts +208 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/url.d.ts +984 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/util.d.ts +2606 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/v8.d.ts +920 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/vm.d.ts +1000 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/wasi.d.ts +181 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/web-globals/abortcontroller.d.ts +34 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/web-globals/domexception.d.ts +68 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/web-globals/events.d.ts +97 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/web-globals/fetch.d.ts +55 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/web-globals/navigator.d.ts +22 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/web-globals/storage.d.ts +24 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/worker_threads.d.ts +784 -0
- package/packages/mailx-send/mailsend/node_modules/@types/node/zlib.d.ts +747 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/LICENSE +21 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/README.md +15 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/index.d.ts +82 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/addressparser/index.d.ts +31 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/base64/index.d.ts +22 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/dkim/index.d.ts +45 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/dkim/message-parser.d.ts +75 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/dkim/relaxed-body.d.ts +75 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/dkim/sign.d.ts +21 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/fetch/cookies.d.ts +54 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/fetch/index.d.ts +38 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/json-transport/index.d.ts +53 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/mail-composer/index.d.ts +25 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/mailer/index.d.ts +283 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/mailer/mail-message.d.ts +32 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/mime-funcs/index.d.ts +87 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/mime-funcs/mime-types.d.ts +2 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/mime-node/index.d.ts +224 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/mime-node/last-newline.d.ts +9 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/qp/index.d.ts +23 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/sendmail-transport/index.d.ts +53 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/sendmail-transport/le-unix.d.ts +7 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/sendmail-transport/le-windows.d.ts +7 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/ses-transport/index.d.ts +146 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/shared/index.d.ts +58 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/smtp-connection/data-stream.d.ts +11 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/smtp-connection/http-proxy-client.d.ts +16 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/smtp-connection/index.d.ts +270 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/smtp-pool/index.d.ts +93 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/smtp-pool/pool-resource.d.ts +66 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/smtp-transport/index.d.ts +115 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/stream-transport/index.d.ts +59 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/well-known/index.d.ts +6 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/xoauth2/index.d.ts +114 -0
- package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/package.json +38 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/.ncurc.js +9 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/.prettierignore +8 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/.prettierrc +12 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/.prettierrc.js +10 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/.release-please-config.json +9 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/LICENSE +16 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/README.md +86 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/SECURITY.txt +22 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/eslint.config.js +88 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/addressparser/index.js +383 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/base64/index.js +139 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/dkim/index.js +253 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/dkim/message-parser.js +155 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/dkim/relaxed-body.js +154 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/dkim/sign.js +117 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/fetch/cookies.js +281 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/fetch/index.js +280 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/json-transport/index.js +82 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mail-composer/index.js +629 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mailer/index.js +441 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mailer/mail-message.js +316 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mime-funcs/index.js +625 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mime-funcs/mime-types.js +2113 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mime-node/index.js +1316 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mime-node/last-newline.js +33 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mime-node/le-unix.js +43 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mime-node/le-windows.js +52 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/nodemailer.js +157 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/punycode/index.js +460 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/qp/index.js +227 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/sendmail-transport/index.js +210 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/ses-transport/index.js +234 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/shared/index.js +754 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/smtp-connection/data-stream.js +108 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +143 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/smtp-connection/index.js +1870 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/smtp-pool/index.js +652 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +259 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/smtp-transport/index.js +421 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/stream-transport/index.js +135 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/well-known/index.js +47 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/well-known/services.json +611 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/xoauth2/index.js +427 -0
- package/packages/mailx-send/mailsend/node_modules/nodemailer/package.json +47 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/LICENSE +21 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/README.md +6 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/agent.d.ts +31 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/api.d.ts +43 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/balanced-pool.d.ts +29 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/cache.d.ts +36 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/client.d.ts +108 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/connector.d.ts +34 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/content-type.d.ts +21 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/cookies.d.ts +28 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/diagnostics-channel.d.ts +66 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/dispatcher.d.ts +256 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/env-http-proxy-agent.d.ts +21 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/errors.d.ts +149 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/eventsource.d.ts +61 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/fetch.d.ts +209 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/file.d.ts +39 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/filereader.d.ts +54 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/formdata.d.ts +108 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/global-dispatcher.d.ts +9 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/global-origin.d.ts +7 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/handlers.d.ts +15 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/header.d.ts +4 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/index.d.ts +71 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/interceptors.d.ts +17 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/mock-agent.d.ts +50 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/mock-client.d.ts +25 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/mock-errors.d.ts +12 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/mock-interceptor.d.ts +93 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/mock-pool.d.ts +25 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/package.json +55 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/patch.d.ts +33 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/pool-stats.d.ts +19 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/pool.d.ts +39 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/proxy-agent.d.ts +28 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/readable.d.ts +65 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/retry-agent.d.ts +8 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/retry-handler.d.ts +116 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/util.d.ts +18 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/webidl.d.ts +228 -0
- package/packages/mailx-send/mailsend/node_modules/undici-types/websocket.d.ts +150 -0
- package/packages/mailx-server/index.js +1 -1
- package/packages/mailx-settings/cloud.js +12 -7
- package/packages/mailx-settings/index.js +22 -1
- package/client/.gitattributes +0 -10
- package/client/app.js.map +0 -1
- package/client/app.ts +0 -3190
- package/client/components/address-book.js.map +0 -1
- package/client/components/address-book.ts +0 -204
- package/client/components/alarms.js.map +0 -1
- package/client/components/alarms.ts +0 -276
- package/client/components/calendar-sidebar.js.map +0 -1
- package/client/components/calendar-sidebar.ts +0 -474
- package/client/components/calendar.js.map +0 -1
- package/client/components/calendar.ts +0 -211
- package/client/components/context-menu.js.map +0 -1
- package/client/components/context-menu.ts +0 -95
- package/client/components/folder-picker.js.map +0 -1
- package/client/components/folder-picker.ts +0 -127
- package/client/components/folder-tree.js.map +0 -1
- package/client/components/folder-tree.ts +0 -1069
- package/client/components/message-list.js.map +0 -1
- package/client/components/message-list.ts +0 -1129
- package/client/components/message-viewer.js.map +0 -1
- package/client/components/message-viewer.ts +0 -1257
- package/client/components/outbox-view.js.map +0 -1
- package/client/components/outbox-view.ts +0 -102
- package/client/components/tasks.js.map +0 -1
- package/client/components/tasks.ts +0 -234
- package/client/compose/compose.js.map +0 -1
- package/client/compose/compose.ts +0 -1231
- package/client/compose/editor.js.map +0 -1
- package/client/compose/editor.ts +0 -599
- package/client/compose/ghost-text.js.map +0 -1
- package/client/compose/ghost-text.ts +0 -140
- package/client/lib/android-bootstrap.js.map +0 -1
- package/client/lib/android-bootstrap.ts +0 -9
- package/client/lib/api-client.js.map +0 -1
- package/client/lib/api-client.ts +0 -439
- package/client/lib/local-service.js.map +0 -1
- package/client/lib/local-service.ts +0 -646
- package/client/lib/local-store.js.map +0 -1
- package/client/lib/local-store.ts +0 -283
- package/client/lib/message-state.js.map +0 -1
- package/client/lib/message-state.ts +0 -160
- package/client/tsconfig.json +0 -19
- package/packages/mailx-api/.gitattributes +0 -10
- package/packages/mailx-api/index.d.ts.map +0 -1
- package/packages/mailx-api/index.js.map +0 -1
- package/packages/mailx-api/index.ts +0 -283
- package/packages/mailx-api/tsconfig.json +0 -9
- package/packages/mailx-compose/.gitattributes +0 -10
- package/packages/mailx-compose/index.d.ts.map +0 -1
- package/packages/mailx-compose/index.js.map +0 -1
- package/packages/mailx-compose/index.ts +0 -85
- package/packages/mailx-compose/tsconfig.json +0 -9
- package/packages/mailx-host/.gitattributes +0 -10
- package/packages/mailx-host/index.d.ts +0 -21
- package/packages/mailx-host/index.d.ts.map +0 -1
- package/packages/mailx-host/index.js +0 -29
- package/packages/mailx-host/index.js.map +0 -1
- package/packages/mailx-host/index.ts +0 -38
- package/packages/mailx-host/package.json +0 -23
- package/packages/mailx-host/tsconfig.json +0 -9
- package/packages/mailx-host/types-shim.d.ts +0 -14
- package/packages/mailx-imap/.gitattributes +0 -10
- package/packages/mailx-imap/index.d.ts +0 -442
- package/packages/mailx-imap/index.d.ts.map +0 -1
- package/packages/mailx-imap/index.js +0 -3684
- package/packages/mailx-imap/index.js.map +0 -1
- package/packages/mailx-imap/index.ts +0 -3652
- package/packages/mailx-imap/package-lock.json +0 -131
- package/packages/mailx-imap/package.json +0 -28
- package/packages/mailx-imap/providers/gmail-api.d.ts +0 -8
- package/packages/mailx-imap/providers/gmail-api.d.ts.map +0 -1
- package/packages/mailx-imap/providers/gmail-api.js +0 -8
- package/packages/mailx-imap/providers/gmail-api.js.map +0 -1
- package/packages/mailx-imap/providers/gmail-api.ts +0 -8
- package/packages/mailx-imap/providers/outlook-api.ts +0 -7
- package/packages/mailx-imap/providers/types.d.ts +0 -9
- package/packages/mailx-imap/providers/types.d.ts.map +0 -1
- package/packages/mailx-imap/providers/types.js +0 -9
- package/packages/mailx-imap/providers/types.js.map +0 -1
- package/packages/mailx-imap/providers/types.ts +0 -9
- package/packages/mailx-imap/tsconfig.json +0 -9
- package/packages/mailx-imap/tsconfig.tsbuildinfo +0 -1
- package/packages/mailx-send/.gitattributes +0 -10
- package/packages/mailx-send/cli-queue.d.ts.map +0 -1
- package/packages/mailx-send/cli-queue.js.map +0 -1
- package/packages/mailx-send/cli-queue.ts +0 -62
- package/packages/mailx-send/cli-send.d.ts.map +0 -1
- package/packages/mailx-send/cli-send.js.map +0 -1
- package/packages/mailx-send/cli-send.ts +0 -83
- package/packages/mailx-send/cli.d.ts.map +0 -1
- package/packages/mailx-send/cli.js.map +0 -1
- package/packages/mailx-send/cli.ts +0 -126
- package/packages/mailx-send/index.d.ts.map +0 -1
- package/packages/mailx-send/index.js.map +0 -1
- package/packages/mailx-send/index.ts +0 -333
- package/packages/mailx-send/mailsend/cli.d.ts.map +0 -1
- package/packages/mailx-send/mailsend/cli.js.map +0 -1
- package/packages/mailx-send/mailsend/cli.ts +0 -81
- package/packages/mailx-send/mailsend/index.d.ts.map +0 -1
- package/packages/mailx-send/mailsend/index.js.map +0 -1
- package/packages/mailx-send/mailsend/index.ts +0 -333
- package/packages/mailx-send/mailsend/tsconfig.json +0 -21
- package/packages/mailx-send/package-lock.json +0 -65
- package/packages/mailx-send/tsconfig.json +0 -21
- package/packages/mailx-server/.gitattributes +0 -10
- package/packages/mailx-server/index.d.ts.map +0 -1
- package/packages/mailx-server/index.js.map +0 -1
- package/packages/mailx-server/index.ts +0 -429
- package/packages/mailx-server/tsconfig.json +0 -9
- package/packages/mailx-settings/.gitattributes +0 -10
- package/packages/mailx-settings/cloud.d.ts.map +0 -1
- package/packages/mailx-settings/cloud.js.map +0 -1
- package/packages/mailx-settings/cloud.ts +0 -388
- package/packages/mailx-settings/index.d.ts.map +0 -1
- package/packages/mailx-settings/index.js.map +0 -1
- package/packages/mailx-settings/index.ts +0 -892
- package/packages/mailx-settings/tsconfig.json +0 -9
- package/packages/mailx-store/.gitattributes +0 -10
- package/packages/mailx-store/db.d.ts.map +0 -1
- package/packages/mailx-store/db.js.map +0 -1
- package/packages/mailx-store/db.ts +0 -2007
- package/packages/mailx-store/file-store.d.ts.map +0 -1
- package/packages/mailx-store/file-store.js.map +0 -1
- package/packages/mailx-store/file-store.ts +0 -82
- package/packages/mailx-store/index.d.ts.map +0 -1
- package/packages/mailx-store/index.js.map +0 -1
- package/packages/mailx-store/index.ts +0 -7
- package/packages/mailx-store/tsconfig.json +0 -9
- package/packages/mailx-types/.gitattributes +0 -10
- package/packages/mailx-types/index.d.ts.map +0 -1
- package/packages/mailx-types/index.js.map +0 -1
- package/packages/mailx-types/index.ts +0 -498
- package/packages/mailx-types/tsconfig.json +0 -9
|
@@ -1,1129 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Message list component — renders paginated message rows.
|
|
3
|
-
* Reads from message-state; operations mutate state, list reacts.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { getMessages as apiGetMessages, getUnifiedInbox as apiGetUnifiedInbox, searchMessages, abortMessageListRequests, updateFlags, getThreadMessages, moveMessages as apiMoveMessages } from "../lib/api-client.js";
|
|
7
|
-
import * as state from "../lib/message-state.js";
|
|
8
|
-
import type { ListMessage } from "../lib/message-state.js";
|
|
9
|
-
import { showContextMenu, type MenuItem } from "./context-menu.js";
|
|
10
|
-
import { pickFolder } from "./folder-picker.js";
|
|
11
|
-
|
|
12
|
-
type MessageSelectHandler = (accountId: string, uid: number, folderId: number) => void;
|
|
13
|
-
|
|
14
|
-
let onMessageSelect: MessageSelectHandler;
|
|
15
|
-
let currentAccountId: string;
|
|
16
|
-
let currentFolderId: number;
|
|
17
|
-
let currentSpecialUse = ""; // Cached for reloads — an empty value on reload used to reset Sent/Drafts/Outbox to "From"
|
|
18
|
-
let lastClickedRow: HTMLElement | null = null;
|
|
19
|
-
let currentPage: number;
|
|
20
|
-
let totalMessages: number;
|
|
21
|
-
let loading = false;
|
|
22
|
-
let unifiedMode = false;
|
|
23
|
-
let searchMode = false;
|
|
24
|
-
let currentSearchQuery = "";
|
|
25
|
-
let showToInsteadOfFrom = false;
|
|
26
|
-
let touchWasScroll = false;
|
|
27
|
-
// Current sort column/direction — cycled by clicking the ml-header columns.
|
|
28
|
-
// "date desc" is the default (newest first). Clicking a column flips direction
|
|
29
|
-
// if it's already active, or switches to that column with its own default dir
|
|
30
|
-
// (text columns default asc, date defaults desc).
|
|
31
|
-
let currentSort = "date";
|
|
32
|
-
let currentSortDir: "asc" | "desc" = "desc";
|
|
33
|
-
|
|
34
|
-
/** S56 refactor slice — per-row focus/unfocus.
|
|
35
|
-
*
|
|
36
|
-
* Every rendered message row has an associated Row record tracking the DOM
|
|
37
|
-
* element + account/msg pair. `focusRow` runs the atomic transition:
|
|
38
|
-
*
|
|
39
|
-
* 1. unfocus the previously-focused row (clears `.selected` class)
|
|
40
|
-
* 2. mark the new row `.selected`
|
|
41
|
-
* 3. update shared state.selected + notify viewer
|
|
42
|
-
*
|
|
43
|
-
* The viewer's `showMessageGeneration` token still cancels stale body
|
|
44
|
-
* fetches during the transition — that's the "async cancellation" piece of
|
|
45
|
-
* S56. The "atomic DOM transition" piece is now owned here.
|
|
46
|
-
*
|
|
47
|
-
* Full abort-signal plumbing through getMessage → fetchMessageBody is a
|
|
48
|
-
* separate follow-up; for now the gen token does the job. */
|
|
49
|
-
let currentFocusedRow: HTMLElement | null = null;
|
|
50
|
-
|
|
51
|
-
function focusRow(row: HTMLElement, accountId: string, msg: any): void {
|
|
52
|
-
if (currentFocusedRow && currentFocusedRow !== row) {
|
|
53
|
-
// Unfocus the previous atomically — clearing .selected AND
|
|
54
|
-
// triggering the viewer's stale-fetch cancel (via the bump inside
|
|
55
|
-
// the next showMessage() call).
|
|
56
|
-
currentFocusedRow.classList.remove("selected");
|
|
57
|
-
}
|
|
58
|
-
if (!row.classList.contains("selected")) row.classList.add("selected");
|
|
59
|
-
currentFocusedRow = row;
|
|
60
|
-
state.select(msg);
|
|
61
|
-
onMessageSelect(accountId, msg.uid, msg.folderId);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Back-compat shim — some call sites still use `focusMessage(accountId, msg)`
|
|
65
|
-
* without a DOM row (e.g. thread-popup click). Fall back to the previous
|
|
66
|
-
* behavior (state + viewer only) so nothing regresses. */
|
|
67
|
-
function focusMessage(accountId: string, msg: any): void {
|
|
68
|
-
state.select(msg);
|
|
69
|
-
onMessageSelect(accountId, msg.uid, msg.folderId);
|
|
70
|
-
// Clear the focused-row reference since we don't have a DOM row here
|
|
71
|
-
// — the next row click will unfocus whatever was selected anyway.
|
|
72
|
-
if (currentFocusedRow) currentFocusedRow.classList.remove("selected");
|
|
73
|
-
currentFocusedRow = null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Flip the "not-downloaded" indicator off for rows whose bodies just cached.
|
|
77
|
-
* Called from the bodyCached service event — covers both background prefetch
|
|
78
|
-
* and on-demand fetch. No-op for rows not currently rendered. */
|
|
79
|
-
export function markBodiesCached(items: { accountId: string; uid: number }[]): void {
|
|
80
|
-
const body = document.getElementById("ml-body");
|
|
81
|
-
if (!body || items.length === 0) return;
|
|
82
|
-
for (const { accountId, uid } of items) {
|
|
83
|
-
const row = body.querySelector(`.ml-row[data-uid="${uid}"][data-account-id="${CSS.escape(accountId)}"]`)
|
|
84
|
-
|| body.querySelector(`.ml-row[data-uid="${uid}"]`);
|
|
85
|
-
if (row) row.classList.remove("not-downloaded");
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Get all selected message rows */
|
|
90
|
-
export function getSelectedMessages(): { accountId: string; uid: number; folderId: number }[] {
|
|
91
|
-
const body = document.getElementById("ml-body");
|
|
92
|
-
if (!body) return [];
|
|
93
|
-
const rows = body.querySelectorAll(".ml-row.selected");
|
|
94
|
-
return Array.from(rows).map(r => ({
|
|
95
|
-
accountId: (r as HTMLElement).dataset.accountId || "",
|
|
96
|
-
uid: Number((r as HTMLElement).dataset.uid),
|
|
97
|
-
folderId: Number((r as HTMLElement).dataset.folderId),
|
|
98
|
-
}));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function clearSelection(): void {
|
|
102
|
-
const body = document.getElementById("ml-body");
|
|
103
|
-
if (body) body.querySelectorAll(".ml-row.selected").forEach(r => r.classList.remove("selected"));
|
|
104
|
-
// S56 seam: the focused-row invariant is "the Row whose .selected is
|
|
105
|
-
// currently mine". clearSelection wipes all .selected, so the invariant
|
|
106
|
-
// would break if we kept a stale pointer.
|
|
107
|
-
currentFocusedRow = null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Deterministic sender-avatar color from a seed string (typically the
|
|
111
|
-
* email address). Hash → hue at 12 evenly-spaced positions on the wheel.
|
|
112
|
-
* Saturation + lightness fixed so all colors carry the same visual weight
|
|
113
|
-
* regardless of hue, and so light/dark themes don't have to override. */
|
|
114
|
-
function senderColor(seed: string): string {
|
|
115
|
-
let h = 0;
|
|
116
|
-
for (let i = 0; i < seed.length; i++) h = (h * 31 + seed.charCodeAt(i)) | 0;
|
|
117
|
-
const hue = ((Math.abs(h) % 12) * 30) + 15; // 15, 45, 75, …, 345
|
|
118
|
-
return `oklch(0.62 0.14 ${hue})`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Exit multi-select mode (entered via touch long-press). Clears selection
|
|
122
|
-
* and the sticky body flag so subsequent taps open messages again. */
|
|
123
|
-
function exitMultiSelect(): void {
|
|
124
|
-
const body = document.getElementById("ml-body");
|
|
125
|
-
if (!body?.classList.contains("multi-select-on")) return;
|
|
126
|
-
body.classList.remove("multi-select-on");
|
|
127
|
-
clearSelection();
|
|
128
|
-
updateBulkBar();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Bulk-actions bar retired 2026-04-24 — trash + spam live on the main
|
|
132
|
-
* toolbar now and every other bulk op (mark-read, flag, move) is one
|
|
133
|
-
* right-click away. Kept as a no-op stub so existing call sites
|
|
134
|
-
* (avatar tap, row click, long-press) don't need to be touched. */
|
|
135
|
-
function updateBulkBar(): void { /* bar removed; nothing to render */ }
|
|
136
|
-
|
|
137
|
-
// Escape key + click-outside-list exit multi-select mode. Attached once
|
|
138
|
-
// (idempotent because document only has one listener scope per handler).
|
|
139
|
-
if (!(window as any).__mailxMultiSelectWired) {
|
|
140
|
-
(window as any).__mailxMultiSelectWired = true;
|
|
141
|
-
document.addEventListener("keydown", (e) => {
|
|
142
|
-
if (e.key === "Escape") exitMultiSelect();
|
|
143
|
-
});
|
|
144
|
-
document.addEventListener("pointerdown", (e) => {
|
|
145
|
-
const body = document.getElementById("ml-body");
|
|
146
|
-
if (!body?.classList.contains("multi-select-on")) return;
|
|
147
|
-
const target = e.target as HTMLElement;
|
|
148
|
-
// A tap on a row is handled by the row's own click listener.
|
|
149
|
-
// The toolbar must also be exempt: its trash / spam / etc.
|
|
150
|
-
// buttons operate ON the current multi-selection, so a tap on
|
|
151
|
-
// them should NOT clear selection before the button's click
|
|
152
|
-
// handler runs (otherwise getSelectedMessages returns empty
|
|
153
|
-
// and the action no-ops — Android-reported 2026-04-30: "press
|
|
154
|
-
// multiple circles, press trashcan, checks vanish, nothing
|
|
155
|
-
// deleted"). Same logic for the folder-tree (drop targets,
|
|
156
|
-
// future: bulk move). Exit only on a tap to genuine neutral
|
|
157
|
-
// ground.
|
|
158
|
-
// Exempt: rows (handled by their own listener), toolbar buttons
|
|
159
|
-
// (delete/spam/etc. operate ON the selection — clearing it here
|
|
160
|
-
// empties the selection before the click runs), folder-tree
|
|
161
|
-
// (drop targets / future bulk move), and the context menu
|
|
162
|
-
// (right-click → "mark read" / "move to" / etc. all need the
|
|
163
|
-
// selection intact when the menu item runs).
|
|
164
|
-
if (target.closest(".ml-row, .toolbar, .folder-tree, .ctx-menu, #btn-tb-delete, #btn-tb-spam")) return;
|
|
165
|
-
exitMultiSelect();
|
|
166
|
-
}, true);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function selectRange(from: HTMLElement, to: HTMLElement): void {
|
|
170
|
-
const body = document.getElementById("ml-body");
|
|
171
|
-
if (!body) return;
|
|
172
|
-
const rows = Array.from(body.querySelectorAll(".ml-row"));
|
|
173
|
-
const fromIdx = rows.indexOf(from);
|
|
174
|
-
const toIdx = rows.indexOf(to);
|
|
175
|
-
if (fromIdx < 0 || toIdx < 0) return;
|
|
176
|
-
const lo = Math.min(fromIdx, toIdx);
|
|
177
|
-
const hi = Math.max(fromIdx, toIdx);
|
|
178
|
-
for (let i = lo; i <= hi; i++) rows[i].classList.add("selected");
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** The row to anchor a shift-click range against. `lastClickedRow` is the
|
|
182
|
-
* primary anchor, but it can become a detached DOM node after a list
|
|
183
|
-
* re-render (folder switch, sort, search reload, paging) — `selectRange`
|
|
184
|
-
* would then no-op. Fall back to whichever live row is `.selected` (the
|
|
185
|
-
* one in the viewer) before giving up. */
|
|
186
|
-
function resolveShiftAnchor(): HTMLElement | null {
|
|
187
|
-
if (lastClickedRow?.isConnected) return lastClickedRow;
|
|
188
|
-
const body = document.getElementById("ml-body");
|
|
189
|
-
if (!body) return null;
|
|
190
|
-
return body.querySelector(".ml-row.selected") as HTMLElement | null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const timeFmt: Intl.DateTimeFormatOptions = { hour: "2-digit", minute: "2-digit", hour12: false };
|
|
194
|
-
const dateFmt: Intl.DateTimeFormatOptions = { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false };
|
|
195
|
-
const dateFmtSameYear: Intl.DateTimeFormatOptions = { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false };
|
|
196
|
-
|
|
197
|
-
export function initMessageList(handler: MessageSelectHandler): void {
|
|
198
|
-
onMessageSelect = handler;
|
|
199
|
-
|
|
200
|
-
// Infinite scroll
|
|
201
|
-
const body = document.getElementById("ml-body");
|
|
202
|
-
if (body) {
|
|
203
|
-
// Touch scroll vs tap: the WebView occasionally synthesizes a click on
|
|
204
|
-
// touchend even when the user clearly scrolled, which opened a message
|
|
205
|
-
// just from swiping the list. Multi-signal detection so a scroll is
|
|
206
|
-
// reliably classified:
|
|
207
|
-
// 1. touchmove movement ≥ TAP_SLOP — the primary signal
|
|
208
|
-
// 2. actual scrollTop change between touchstart and touchend — always
|
|
209
|
-
// set the flag when the container moved, even if touchmove never
|
|
210
|
-
// fired (some Android builds coalesce events under momentum)
|
|
211
|
-
// 3. longer TAP_SLOP (15px) — fingers are wide; 10px was too twitchy
|
|
212
|
-
let touchStartY = 0;
|
|
213
|
-
let touchStartX = 0;
|
|
214
|
-
let touchStartScrollTop = 0;
|
|
215
|
-
const TAP_SLOP = 15;
|
|
216
|
-
body.addEventListener("touchstart", (e) => {
|
|
217
|
-
const t = e.touches[0];
|
|
218
|
-
touchStartY = t.clientY;
|
|
219
|
-
touchStartX = t.clientX;
|
|
220
|
-
touchStartScrollTop = body.scrollTop;
|
|
221
|
-
touchWasScroll = false;
|
|
222
|
-
}, { passive: true });
|
|
223
|
-
body.addEventListener("touchmove", (e) => {
|
|
224
|
-
const t = e.touches[0];
|
|
225
|
-
if (Math.abs(t.clientY - touchStartY) > TAP_SLOP || Math.abs(t.clientX - touchStartX) > TAP_SLOP) {
|
|
226
|
-
touchWasScroll = true;
|
|
227
|
-
}
|
|
228
|
-
}, { passive: true });
|
|
229
|
-
body.addEventListener("touchend", () => {
|
|
230
|
-
// If the container actually scrolled during this touch, the user
|
|
231
|
-
// was scrolling regardless of how small their finger movement was.
|
|
232
|
-
if (body.scrollTop !== touchStartScrollTop) {
|
|
233
|
-
touchWasScroll = true;
|
|
234
|
-
}
|
|
235
|
-
}, { passive: true });
|
|
236
|
-
|
|
237
|
-
body.addEventListener("scroll", () => {
|
|
238
|
-
if (loading) return;
|
|
239
|
-
const nearBottom = body.scrollHeight - body.scrollTop - body.clientHeight < 200;
|
|
240
|
-
if (nearBottom && currentPage * 50 < totalMessages) {
|
|
241
|
-
loadMoreMessages();
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Subscribe to state changes — react to removeMessages (move/delete)
|
|
247
|
-
state.subscribe((change) => {
|
|
248
|
-
if (change === "removed") {
|
|
249
|
-
syncDomToState();
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// Sort column headers — click to cycle. Date defaults desc (newest first);
|
|
254
|
-
// From/Subject default asc on first click so alphabetical order reads
|
|
255
|
-
// naturally. Clicking the currently-active column flips direction.
|
|
256
|
-
const header = document.getElementById("ml-header");
|
|
257
|
-
if (header) {
|
|
258
|
-
header.addEventListener("click", (e) => {
|
|
259
|
-
const col = (e.target as HTMLElement).closest<HTMLElement>(".ml-col-sortable");
|
|
260
|
-
if (!col) return;
|
|
261
|
-
const key = col.dataset.sort;
|
|
262
|
-
if (!key) return;
|
|
263
|
-
if (currentSort === key) {
|
|
264
|
-
currentSortDir = currentSortDir === "asc" ? "desc" : "asc";
|
|
265
|
-
} else {
|
|
266
|
-
currentSort = key;
|
|
267
|
-
currentSortDir = key === "date" ? "desc" : "asc";
|
|
268
|
-
}
|
|
269
|
-
// Only per-folder lists support server-side sort today; unified and
|
|
270
|
-
// search paths sort client-side on the fetched page. Reload if we
|
|
271
|
-
// have an active per-folder context.
|
|
272
|
-
if (!searchMode && !unifiedMode && currentAccountId && currentFolderId) {
|
|
273
|
-
loadMessages(currentAccountId, currentFolderId, 1, "", false);
|
|
274
|
-
} else {
|
|
275
|
-
applyClientSideSort();
|
|
276
|
-
updateSortIndicators();
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
updateSortIndicators();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/** Reorder currently-loaded state messages in-place by currentSort/currentSortDir.
|
|
284
|
-
* Used for unified-inbox and search results where the server can't re-sort
|
|
285
|
-
* a single page on our behalf. */
|
|
286
|
-
function applyClientSideSort(): void {
|
|
287
|
-
const items = [...state.getMessages()];
|
|
288
|
-
const sign = currentSortDir === "asc" ? 1 : -1;
|
|
289
|
-
items.sort((a: any, b: any) => {
|
|
290
|
-
if (currentSort === "from") {
|
|
291
|
-
const av = (a.from?.name || a.from?.address || "").toLowerCase();
|
|
292
|
-
const bv = (b.from?.name || b.from?.address || "").toLowerCase();
|
|
293
|
-
return av < bv ? -sign : av > bv ? sign : 0;
|
|
294
|
-
}
|
|
295
|
-
if (currentSort === "subject") {
|
|
296
|
-
const av = (a.subject || "").replace(/^(re:|fwd:|fw:)\s*/i, "").toLowerCase();
|
|
297
|
-
const bv = (b.subject || "").replace(/^(re:|fwd:|fw:)\s*/i, "").toLowerCase();
|
|
298
|
-
return av < bv ? -sign : av > bv ? sign : 0;
|
|
299
|
-
}
|
|
300
|
-
// date
|
|
301
|
-
return ((a.date || 0) - (b.date || 0)) * sign;
|
|
302
|
-
});
|
|
303
|
-
state.setMessages(items as any);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function updateSortIndicators(): void {
|
|
307
|
-
const header = document.getElementById("ml-header");
|
|
308
|
-
if (!header) return;
|
|
309
|
-
header.querySelectorAll<HTMLElement>(".ml-col-sortable").forEach(c => {
|
|
310
|
-
c.classList.remove("ml-col-sort-asc", "ml-col-sort-desc");
|
|
311
|
-
if (c.dataset.sort === currentSort) {
|
|
312
|
-
c.classList.add(currentSortDir === "asc" ? "ml-col-sort-asc" : "ml-col-sort-desc");
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Sync DOM rows to current state after messages are removed.
|
|
319
|
-
* Removes DOM rows that are no longer in state, updates selection.
|
|
320
|
-
*/
|
|
321
|
-
function syncDomToState(): void {
|
|
322
|
-
const body = document.getElementById("ml-body");
|
|
323
|
-
if (!body) return;
|
|
324
|
-
|
|
325
|
-
// Build set of UIDs still in state
|
|
326
|
-
const stateUids = new Set(state.getMessages().map(m => `${m.accountId}:${m.uid}`));
|
|
327
|
-
|
|
328
|
-
// Remove rows not in state
|
|
329
|
-
for (const row of Array.from(body.querySelectorAll(".ml-row"))) {
|
|
330
|
-
const el = row as HTMLElement;
|
|
331
|
-
const key = `${el.dataset.accountId}:${el.dataset.uid}`;
|
|
332
|
-
if (!stateUids.has(key)) {
|
|
333
|
-
el.remove();
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Update selection to match state
|
|
338
|
-
clearSelection();
|
|
339
|
-
const sel = state.getSelected();
|
|
340
|
-
if (sel) {
|
|
341
|
-
const row = body.querySelector(`.ml-row[data-uid="${sel.uid}"][data-account-id="${sel.accountId}"]`) as HTMLElement
|
|
342
|
-
|| body.querySelector(`.ml-row[data-uid="${sel.uid}"]`) as HTMLElement;
|
|
343
|
-
if (row) {
|
|
344
|
-
row.classList.add("selected");
|
|
345
|
-
lastClickedRow = row;
|
|
346
|
-
// Trigger viewer update
|
|
347
|
-
onMessageSelect(sel.accountId, sel.uid, sel.folderId);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// If no messages left, show empty
|
|
352
|
-
if (state.getMessages().length === 0) {
|
|
353
|
-
body.innerHTML = `<div class="ml-empty">No messages</div>`;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/** Reload the currently displayed folder (preserves current selection) */
|
|
358
|
-
export function reloadCurrentFolder(): void {
|
|
359
|
-
if (searchMode) {
|
|
360
|
-
loadSearchResults(currentSearchQuery);
|
|
361
|
-
} else if (unifiedMode) {
|
|
362
|
-
loadUnifiedInbox(false);
|
|
363
|
-
} else if (currentAccountId && currentFolderId) {
|
|
364
|
-
loadMessages(currentAccountId, currentFolderId, 1, "", false);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/** Exit search mode without triggering a reload — caller decides what to load
|
|
369
|
-
* next. Used by the search-input "cleared to empty" handler so the next
|
|
370
|
-
* reloadCurrentFolder() call doesn't re-run the stale search query. */
|
|
371
|
-
export function clearSearchMode(): void {
|
|
372
|
-
searchMode = false;
|
|
373
|
-
currentSearchQuery = "";
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/** Load unified inbox (all accounts) */
|
|
377
|
-
export async function loadUnifiedInbox(autoSelect = true): Promise<void> {
|
|
378
|
-
unifiedMode = true;
|
|
379
|
-
searchMode = false;
|
|
380
|
-
currentSpecialUse = "";
|
|
381
|
-
showToInsteadOfFrom = false; // Unified inbox always shows From, not To
|
|
382
|
-
currentPage = 1;
|
|
383
|
-
totalMessages = 0;
|
|
384
|
-
|
|
385
|
-
const body = document.getElementById("ml-body");
|
|
386
|
-
if (!body) return;
|
|
387
|
-
const fromHeader = document.querySelector(".ml-col-from");
|
|
388
|
-
if (fromHeader) fromHeader.textContent = "From";
|
|
389
|
-
|
|
390
|
-
const savedScroll = !autoSelect ? body.scrollTop : 0;
|
|
391
|
-
const savedUid = !autoSelect ? body.querySelector(".ml-row.selected")?.getAttribute("data-uid") : null;
|
|
392
|
-
|
|
393
|
-
if (autoSelect) {
|
|
394
|
-
body.innerHTML = `<div class="ml-empty">Loading...</div>`;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
try {
|
|
398
|
-
const result = await apiGetUnifiedInbox(1);
|
|
399
|
-
totalMessages = result.total;
|
|
400
|
-
|
|
401
|
-
if (result.items.length === 0) {
|
|
402
|
-
state.setMessages([]);
|
|
403
|
-
body.innerHTML = `<div class="ml-empty">${result.total > 0 ? `${result.total} messages syncing...` : "Syncing — messages will appear shortly"}</div>`;
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
state.setMessages(result.items);
|
|
408
|
-
renderMessages(body, "", result.items);
|
|
409
|
-
|
|
410
|
-
if (autoSelect) {
|
|
411
|
-
selectFirst(body);
|
|
412
|
-
} else {
|
|
413
|
-
body.scrollTop = savedScroll;
|
|
414
|
-
restoreSelection(body, savedUid);
|
|
415
|
-
}
|
|
416
|
-
} catch (e: any) {
|
|
417
|
-
if (e.name === "AbortError") return;
|
|
418
|
-
body.innerHTML = `<div class="ml-empty">Error: ${e.message}</div>`;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/** Load search results */
|
|
423
|
-
export async function loadSearchResults(query: string, scope = "all", accountId = "", folderId = 0): Promise<void> {
|
|
424
|
-
searchMode = true;
|
|
425
|
-
unifiedMode = false;
|
|
426
|
-
currentSearchQuery = query;
|
|
427
|
-
currentPage = 1;
|
|
428
|
-
totalMessages = 0;
|
|
429
|
-
|
|
430
|
-
const body = document.getElementById("ml-body");
|
|
431
|
-
if (!body) return;
|
|
432
|
-
|
|
433
|
-
// Clear the preview pane — old preview from the prior selection lingers
|
|
434
|
-
// until the user clicks a search result. Atomic clear-on-list-mutation
|
|
435
|
-
// is what Slice D's row-objects-own-preview will do generally; this is
|
|
436
|
-
// the band-aid for the search-specific case until that lands.
|
|
437
|
-
document.dispatchEvent(new CustomEvent("mailx-clear-viewer"));
|
|
438
|
-
|
|
439
|
-
body.innerHTML = `<div class="ml-empty">Searching...</div>`;
|
|
440
|
-
|
|
441
|
-
try {
|
|
442
|
-
// Regex search: filter client-side
|
|
443
|
-
if (query.startsWith("/") && query.endsWith("/") && query.length > 2) {
|
|
444
|
-
const pattern = query.slice(1, -1);
|
|
445
|
-
let regex: RegExp;
|
|
446
|
-
try { regex = new RegExp(pattern, "i"); } catch { body.innerHTML = `<div class="ml-empty">Invalid regex</div>`; return; }
|
|
447
|
-
|
|
448
|
-
const source = scope === "current" && accountId
|
|
449
|
-
? await apiGetMessages(accountId, folderId, 1, 10000)
|
|
450
|
-
: await apiGetUnifiedInbox(1, 10000);
|
|
451
|
-
const matches = source.items.filter((m: any) =>
|
|
452
|
-
regex.test(m.subject || "") || regex.test(m.from?.name || "") || regex.test(m.from?.address || "") || regex.test(m.preview || "")
|
|
453
|
-
);
|
|
454
|
-
totalMessages = matches.length;
|
|
455
|
-
state.setMessages(matches);
|
|
456
|
-
if (matches.length === 0) { body.innerHTML = `<div class="ml-empty">No regex matches</div>`; return; }
|
|
457
|
-
body.innerHTML = "";
|
|
458
|
-
appendMessages(body, "", matches);
|
|
459
|
-
selectFirst(body);
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const result = await searchMessages(query, 1, 50, scope, accountId, folderId);
|
|
464
|
-
totalMessages = result.total;
|
|
465
|
-
|
|
466
|
-
if (result.items.length === 0) {
|
|
467
|
-
state.setMessages([]);
|
|
468
|
-
body.innerHTML = `<div class="ml-empty">No results for "${query}"</div>`;
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
state.setMessages(result.items);
|
|
473
|
-
body.innerHTML = "";
|
|
474
|
-
appendMessages(body, "", result.items);
|
|
475
|
-
} catch (e: any) {
|
|
476
|
-
body.innerHTML = `<div class="ml-empty">Search error: ${e.message}</div>`;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
export async function loadMessages(accountId: string, folderId: number, page = 1, specialUse = "", autoSelect = true): Promise<void> {
|
|
481
|
-
searchMode = false;
|
|
482
|
-
unifiedMode = false;
|
|
483
|
-
// Folder switch clears any in-progress multi-select — carrying a "3
|
|
484
|
-
// selected" state across folders would lie about what rows the bulk
|
|
485
|
-
// buttons would act on.
|
|
486
|
-
exitMultiSelect();
|
|
487
|
-
// specialUse is either the DB tag ("sent"/"drafts"/"outbox") or the
|
|
488
|
-
// folder path lowercased (folder-tree fallback when tag is missing — common
|
|
489
|
-
// on Dovecot which doesn't advertise \Sent). Match both cases.
|
|
490
|
-
// Empty specialUse on reload means "keep what we had" — otherwise a
|
|
491
|
-
// folderCountsChanged event or sort-header click resets Sent/Drafts/Outbox
|
|
492
|
-
// back to showing From (user-reported regression 2026-04-24).
|
|
493
|
-
if (specialUse) currentSpecialUse = specialUse;
|
|
494
|
-
const su = currentSpecialUse.toLowerCase();
|
|
495
|
-
showToInsteadOfFrom = su === "sent" || su === "drafts" || su === "outbox"
|
|
496
|
-
|| su.endsWith("sent") || su.endsWith("drafts") || su.endsWith("outbox")
|
|
497
|
-
|| su === "sent items" || su === "sent mail" || su.endsWith("/sent items") || su.endsWith(".sent items");
|
|
498
|
-
currentAccountId = accountId;
|
|
499
|
-
currentFolderId = folderId;
|
|
500
|
-
currentPage = 1;
|
|
501
|
-
totalMessages = 0;
|
|
502
|
-
|
|
503
|
-
const body = document.getElementById("ml-body");
|
|
504
|
-
if (!body) return;
|
|
505
|
-
|
|
506
|
-
// Update header label
|
|
507
|
-
const fromHeader = document.querySelector(".ml-col-from");
|
|
508
|
-
if (fromHeader) fromHeader.textContent = showToInsteadOfFrom ? "To" : "From";
|
|
509
|
-
|
|
510
|
-
const savedScroll = !autoSelect ? body.scrollTop : 0;
|
|
511
|
-
const savedUid = !autoSelect ? body.querySelector(".ml-row.selected")?.getAttribute("data-uid") : null;
|
|
512
|
-
|
|
513
|
-
if (autoSelect) {
|
|
514
|
-
body.innerHTML = `<div class="ml-empty">Loading...</div>`;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
try {
|
|
518
|
-
const flaggedOnly = document.getElementById("ml-body")?.classList.contains("flagged-only") || false;
|
|
519
|
-
const result = await apiGetMessages(accountId, folderId, 1, 50, flaggedOnly, currentSort, currentSortDir);
|
|
520
|
-
totalMessages = result.total;
|
|
521
|
-
updateSortIndicators();
|
|
522
|
-
|
|
523
|
-
if (result.items.length === 0) {
|
|
524
|
-
state.setMessages([]);
|
|
525
|
-
body.innerHTML = `<div class="ml-empty">${flaggedOnly ? "No flagged messages" : "No messages"}</div>`;
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
state.setMessages(result.items);
|
|
530
|
-
renderMessages(body, accountId, result.items);
|
|
531
|
-
|
|
532
|
-
if (autoSelect) {
|
|
533
|
-
selectFirst(body);
|
|
534
|
-
} else {
|
|
535
|
-
requestAnimationFrame(() => {
|
|
536
|
-
body.scrollTop = savedScroll;
|
|
537
|
-
restoreSelection(body, savedUid);
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
} catch (e: any) {
|
|
541
|
-
if (e.name === "AbortError") return;
|
|
542
|
-
body.innerHTML = `<div class="ml-empty">Error: ${e.message}</div>`;
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
async function loadMoreMessages(): Promise<void> {
|
|
547
|
-
const body = document.getElementById("ml-body");
|
|
548
|
-
if (!body) return;
|
|
549
|
-
|
|
550
|
-
loading = true;
|
|
551
|
-
currentPage++;
|
|
552
|
-
|
|
553
|
-
try {
|
|
554
|
-
const flaggedOnly = body.classList.contains("flagged-only");
|
|
555
|
-
const result = searchMode
|
|
556
|
-
? await searchMessages(currentSearchQuery, currentPage)
|
|
557
|
-
: unifiedMode
|
|
558
|
-
? await apiGetUnifiedInbox(currentPage)
|
|
559
|
-
: await apiGetMessages(currentAccountId, currentFolderId, currentPage, 50, flaggedOnly);
|
|
560
|
-
// Append to state
|
|
561
|
-
const current = state.getMessages();
|
|
562
|
-
state.setMessages([...current, ...result.items]);
|
|
563
|
-
appendMessages(body, unifiedMode ? "" : currentAccountId, result.items);
|
|
564
|
-
} catch (e: any) {
|
|
565
|
-
console.error(`Load more error: ${e.message}`);
|
|
566
|
-
} finally {
|
|
567
|
-
loading = false;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/** Replace body contents with rendered rows */
|
|
572
|
-
function renderMessages(body: HTMLElement, accountId: string, items: any[]): void {
|
|
573
|
-
const fragment = document.createDocumentFragment();
|
|
574
|
-
const tempDiv = document.createElement("div");
|
|
575
|
-
appendMessages(tempDiv, accountId, items);
|
|
576
|
-
while (tempDiv.firstChild) fragment.appendChild(tempDiv.firstChild);
|
|
577
|
-
body.replaceChildren(fragment);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function selectFirst(body: HTMLElement): void {
|
|
581
|
-
// Narrow viewports (Android, phone-sized): don't auto-select. The
|
|
582
|
-
// click handler in app.ts switches the layout to "narrow-active" on
|
|
583
|
-
// any list-row click, which on a phone means the message viewer takes
|
|
584
|
-
// over the screen and hides the list. Auto-selecting at startup
|
|
585
|
-
// therefore lands the user in the LAST letter they read instead of
|
|
586
|
-
// the inbox summary they wanted. Desktop unchanged — auto-select
|
|
587
|
-
// remains useful when the list and viewer are side-by-side.
|
|
588
|
-
if (window.innerWidth <= 768) return;
|
|
589
|
-
const firstRow = body.querySelector(".ml-row") as HTMLElement;
|
|
590
|
-
if (firstRow) firstRow.click();
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function restoreSelection(body: HTMLElement, savedUid: string | null | undefined): void {
|
|
594
|
-
if (savedUid) {
|
|
595
|
-
const row = body.querySelector(`.ml-row[data-uid="${savedUid}"]`) as HTMLElement;
|
|
596
|
-
if (row) row.classList.add("selected");
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/** Show a floating list of all messages in a thread when the pill is clicked.
|
|
601
|
-
* Each entry in the popup selects that message in the viewer when clicked.
|
|
602
|
-
* This is simpler than inline expansion and avoids duplicating the row builder. */
|
|
603
|
-
export async function showThreadPopup(pillEl: HTMLElement, headMsg: any): Promise<void> {
|
|
604
|
-
// Remove any existing popup
|
|
605
|
-
document.querySelectorAll(".ml-thread-popup").forEach(el => el.remove());
|
|
606
|
-
let thread: any[] = [];
|
|
607
|
-
try { thread = await getThreadMessages(headMsg.accountId, headMsg.threadId); } catch { /* ignore */ }
|
|
608
|
-
if (!thread || thread.length === 0) return;
|
|
609
|
-
thread.sort((a, b) => (a.date || 0) - (b.date || 0));
|
|
610
|
-
const popup = document.createElement("div");
|
|
611
|
-
popup.className = "ml-thread-popup";
|
|
612
|
-
for (const msg of thread) {
|
|
613
|
-
const item = document.createElement("div");
|
|
614
|
-
item.className = "ml-thread-popup-item";
|
|
615
|
-
if (!msg.flags.includes("\\Seen")) item.classList.add("unread");
|
|
616
|
-
const from = document.createElement("span");
|
|
617
|
-
from.className = "ml-thread-popup-from";
|
|
618
|
-
from.textContent = msg.from?.name || msg.from?.address || "?";
|
|
619
|
-
const date = document.createElement("span");
|
|
620
|
-
date.className = "ml-thread-popup-date";
|
|
621
|
-
date.textContent = formatDate(msg.date);
|
|
622
|
-
const subject = document.createElement("span");
|
|
623
|
-
subject.className = "ml-thread-popup-subject";
|
|
624
|
-
subject.textContent = msg.subject || "(no subject)";
|
|
625
|
-
item.appendChild(from);
|
|
626
|
-
item.appendChild(date);
|
|
627
|
-
item.appendChild(subject);
|
|
628
|
-
item.addEventListener("click", async () => {
|
|
629
|
-
focusMessage(msg.accountId, { accountId: msg.accountId, uid: msg.uid, folderId: msg.folderId, subject: msg.subject, from: msg.from, to: msg.to, cc: msg.cc, date: msg.date, flags: msg.flags, size: msg.size, preview: msg.preview, hasAttachments: msg.hasAttachments });
|
|
630
|
-
popup.remove();
|
|
631
|
-
});
|
|
632
|
-
popup.appendChild(item);
|
|
633
|
-
}
|
|
634
|
-
document.body.appendChild(popup);
|
|
635
|
-
const rect = pillEl.getBoundingClientRect();
|
|
636
|
-
popup.style.left = `${rect.left}px`;
|
|
637
|
-
popup.style.top = `${rect.bottom + 4}px`;
|
|
638
|
-
// Dismiss on outside click
|
|
639
|
-
setTimeout(() => {
|
|
640
|
-
const dismiss = (e: MouseEvent) => {
|
|
641
|
-
if (!popup.contains(e.target as Node)) {
|
|
642
|
-
popup.remove();
|
|
643
|
-
document.removeEventListener("mousedown", dismiss, true);
|
|
644
|
-
}
|
|
645
|
-
};
|
|
646
|
-
document.addEventListener("mousedown", dismiss, true);
|
|
647
|
-
}, 0);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function appendMessages(body: HTMLElement, accountId: string, items: any[]): void {
|
|
651
|
-
// Thread grouping: when the list has the "threaded" class, collapse messages
|
|
652
|
-
// sharing the same threadId to a single row showing the most recent message,
|
|
653
|
-
// with a small pill indicating the thread size. Pre-threading messages have
|
|
654
|
-
// no threadId — those are treated as singletons keyed by their own uid.
|
|
655
|
-
const threaded = body.classList.contains("threaded");
|
|
656
|
-
let rowsToRender: any[] = items;
|
|
657
|
-
let threadSize: Map<any, number> | null = null;
|
|
658
|
-
if (threaded) {
|
|
659
|
-
const threadMap = new Map<string, any>(); // threadId → newest msg
|
|
660
|
-
threadSize = new Map<any, number>();
|
|
661
|
-
for (const msg of items) {
|
|
662
|
-
const key = msg.threadId || `_msg_${msg.accountId || accountId}_${msg.uid}`;
|
|
663
|
-
const existing = threadMap.get(key);
|
|
664
|
-
if (!existing || (msg.date || 0) > (existing.date || 0)) {
|
|
665
|
-
threadMap.set(key, msg);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
// Count messages per thread
|
|
669
|
-
for (const msg of items) {
|
|
670
|
-
const key = msg.threadId || `_msg_${msg.accountId || accountId}_${msg.uid}`;
|
|
671
|
-
const head = threadMap.get(key);
|
|
672
|
-
if (head) threadSize.set(head, (threadSize.get(head) || 0) + 1);
|
|
673
|
-
}
|
|
674
|
-
rowsToRender = Array.from(threadMap.values()).sort((a, b) => (b.date || 0) - (a.date || 0));
|
|
675
|
-
}
|
|
676
|
-
for (const msg of rowsToRender) {
|
|
677
|
-
const msgAccountId = msg.accountId || accountId;
|
|
678
|
-
const row = document.createElement("div");
|
|
679
|
-
row.className = "ml-row";
|
|
680
|
-
row.draggable = true;
|
|
681
|
-
if (!msg.flags.includes("\\Seen")) row.classList.add("unread");
|
|
682
|
-
if (msg.flags.includes("\\Flagged")) row.classList.add("flagged");
|
|
683
|
-
if (!msg.bodyPath) row.classList.add("not-downloaded");
|
|
684
|
-
// Pink-row visible reconciliation state (S1 slice C): a queued local
|
|
685
|
-
// action (move/flag/delete) hasn't been ACK'd by the server yet.
|
|
686
|
-
if (msg.pending) row.classList.add("pending-reconcile");
|
|
687
|
-
// Reply-row marker: messages with In-Reply-To are replies. Shows a
|
|
688
|
-
// subtle left-border accent so the eye can pick out threaded replies
|
|
689
|
-
// without enabling full thread grouping.
|
|
690
|
-
if (msg.inReplyTo) row.classList.add("is-reply");
|
|
691
|
-
row.dataset.uid = String(msg.uid);
|
|
692
|
-
row.dataset.accountId = msgAccountId;
|
|
693
|
-
row.dataset.folderId = String(msg.folderId);
|
|
694
|
-
if (msg.threadId) row.dataset.threadId = msg.threadId;
|
|
695
|
-
|
|
696
|
-
// Sender avatar \u2014 Thunderbird-style colored circle with the first
|
|
697
|
-
// initial of the sender's display name. Doubles as the multi-select
|
|
698
|
-
// affordance: in `multi-select-on` mode, the avatar swaps to a
|
|
699
|
-
// checkmark via CSS. Color is derived deterministically from the
|
|
700
|
-
// address so the same sender keeps the same color across rows.
|
|
701
|
-
const fromName = (showToInsteadOfFrom && msg.to?.length)
|
|
702
|
-
? (msg.to[0].name || msg.to[0].address || "?")
|
|
703
|
-
: (msg.from?.name || msg.from?.address || "?");
|
|
704
|
-
const seedAddr = (msg.from?.address || msg.from?.name || "?").toLowerCase();
|
|
705
|
-
const initial = (fromName.replace(/^[\W_]+/, "") || "?").charAt(0).toUpperCase();
|
|
706
|
-
const avatar = document.createElement("span");
|
|
707
|
-
avatar.className = "ml-avatar";
|
|
708
|
-
avatar.textContent = initial;
|
|
709
|
-
avatar.style.background = senderColor(seedAddr);
|
|
710
|
-
avatar.title = msg.from?.address || "";
|
|
711
|
-
// Tapping the avatar enters multi-select mode (or toggles in it,
|
|
712
|
-
// mirroring Thunderbird/Gmail). Click bubbles to the row otherwise,
|
|
713
|
-
// which would open the message — stopPropagation here keeps the
|
|
714
|
-
// avatar a dedicated selection affordance.
|
|
715
|
-
avatar.addEventListener("click", (e) => {
|
|
716
|
-
e.stopPropagation();
|
|
717
|
-
const body = document.getElementById("ml-body");
|
|
718
|
-
if (!body) return;
|
|
719
|
-
if (body.classList.contains("multi-select-on")) {
|
|
720
|
-
row.classList.toggle("selected");
|
|
721
|
-
} else {
|
|
722
|
-
clearSelection();
|
|
723
|
-
row.classList.add("selected");
|
|
724
|
-
body.classList.add("multi-select-on");
|
|
725
|
-
}
|
|
726
|
-
lastClickedRow = row;
|
|
727
|
-
updateBulkBar();
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
// Right-click (or long-press) on the avatar → bulk-selection menu.
|
|
731
|
-
// Putting it on the avatar is contextually right: the avatar is the
|
|
732
|
-
// "select" affordance, so its menu owns operations on the selection
|
|
733
|
-
// set. "Select all visible" is the load-bearing item — there's no
|
|
734
|
-
// Ctrl-A equivalent on touch and the scope-after-search use case
|
|
735
|
-
// demands it.
|
|
736
|
-
avatar.addEventListener("contextmenu", async (e) => {
|
|
737
|
-
e.preventDefault();
|
|
738
|
-
e.stopPropagation();
|
|
739
|
-
const { showContextMenu } = await import("./context-menu.js");
|
|
740
|
-
const body = document.getElementById("ml-body");
|
|
741
|
-
const visibleRows = body
|
|
742
|
-
? Array.from(body.querySelectorAll<HTMLElement>(".ml-row:not(.filter-hidden)"))
|
|
743
|
-
: [];
|
|
744
|
-
const selectedCount = body
|
|
745
|
-
? body.querySelectorAll(".ml-row.selected").length
|
|
746
|
-
: 0;
|
|
747
|
-
showContextMenu(e.clientX, e.clientY, [
|
|
748
|
-
{
|
|
749
|
-
label: `Select all (${visibleRows.length})`,
|
|
750
|
-
action: () => {
|
|
751
|
-
if (!body) return;
|
|
752
|
-
body.classList.add("multi-select-on");
|
|
753
|
-
for (const r of visibleRows) r.classList.add("selected");
|
|
754
|
-
lastClickedRow = visibleRows[visibleRows.length - 1] || null;
|
|
755
|
-
updateBulkBar();
|
|
756
|
-
},
|
|
757
|
-
disabled: visibleRows.length === 0,
|
|
758
|
-
},
|
|
759
|
-
{
|
|
760
|
-
label: `Clear selection${selectedCount ? ` (${selectedCount})` : ""}`,
|
|
761
|
-
action: () => exitMultiSelect(),
|
|
762
|
-
disabled: selectedCount === 0,
|
|
763
|
-
},
|
|
764
|
-
{
|
|
765
|
-
label: "Invert selection",
|
|
766
|
-
action: () => {
|
|
767
|
-
if (!body) return;
|
|
768
|
-
body.classList.add("multi-select-on");
|
|
769
|
-
for (const r of visibleRows) r.classList.toggle("selected");
|
|
770
|
-
lastClickedRow = visibleRows[visibleRows.length - 1] || null;
|
|
771
|
-
updateBulkBar();
|
|
772
|
-
},
|
|
773
|
-
disabled: visibleRows.length === 0,
|
|
774
|
-
},
|
|
775
|
-
]);
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
const flag = document.createElement("span");
|
|
779
|
-
flag.className = "ml-flag";
|
|
780
|
-
flag.textContent = msg.flags.includes("\\Flagged") ? "\u2605" : "\u2606";
|
|
781
|
-
flag.title = "Toggle flag";
|
|
782
|
-
|
|
783
|
-
const from = document.createElement("span");
|
|
784
|
-
from.className = "ml-from";
|
|
785
|
-
if (showToInsteadOfFrom && msg.to?.length) {
|
|
786
|
-
from.textContent = msg.to.map((a: any) => a.name || a.address).join(", ");
|
|
787
|
-
} else {
|
|
788
|
-
from.textContent = msg.from.name || msg.from.address;
|
|
789
|
-
}
|
|
790
|
-
if (!accountId && msgAccountId) {
|
|
791
|
-
const tag = document.createElement("span");
|
|
792
|
-
tag.className = "ml-account-tag";
|
|
793
|
-
tag.textContent = msgAccountId.charAt(0).toUpperCase();
|
|
794
|
-
tag.title = msgAccountId;
|
|
795
|
-
from.prepend(tag);
|
|
796
|
-
}
|
|
797
|
-
// Search/cross-folder results carry folderName — show a tag so the user
|
|
798
|
-
// can tell which folder each hit lives in.
|
|
799
|
-
if (msg.folderName) {
|
|
800
|
-
const folderTag = document.createElement("span");
|
|
801
|
-
folderTag.className = "ml-folder-tag";
|
|
802
|
-
folderTag.textContent = msg.folderName;
|
|
803
|
-
folderTag.title = `In folder: ${msg.folderName}`;
|
|
804
|
-
from.prepend(folderTag);
|
|
805
|
-
}
|
|
806
|
-
// Unified inbox: same Message-ID exists under >=2 accounts → ⇆ badge.
|
|
807
|
-
// Tooltip names the count so the user knows "this appears on N".
|
|
808
|
-
if ((msg.dupeCount as number) >= 2) {
|
|
809
|
-
const dupe = document.createElement("span");
|
|
810
|
-
dupe.className = "ml-dupe-tag";
|
|
811
|
-
dupe.textContent = "⇆";
|
|
812
|
-
dupe.title = `Same message on ${msg.dupeCount} accounts`;
|
|
813
|
-
from.prepend(dupe);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const subject = document.createElement("span");
|
|
817
|
-
subject.className = "ml-subject";
|
|
818
|
-
subject.innerHTML = escapeHtml(msg.subject);
|
|
819
|
-
// Thread size pill: click to show a popup list of the thread's messages.
|
|
820
|
-
if (threadSize) {
|
|
821
|
-
const n = threadSize.get(msg) || 1;
|
|
822
|
-
if (n > 1 && msg.threadId) {
|
|
823
|
-
row.classList.add("thread-head");
|
|
824
|
-
row.dataset.threadId = msg.threadId;
|
|
825
|
-
const threadPill = document.createElement("span");
|
|
826
|
-
threadPill.className = "ml-thread-pill";
|
|
827
|
-
threadPill.textContent = String(n);
|
|
828
|
-
threadPill.title = `${n} messages in this thread — click to see list`;
|
|
829
|
-
threadPill.addEventListener("click", async (e) => {
|
|
830
|
-
e.stopPropagation();
|
|
831
|
-
await showThreadPopup(threadPill, msg);
|
|
832
|
-
});
|
|
833
|
-
subject.prepend(threadPill);
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
if (msg.preview) {
|
|
837
|
-
const preview = document.createElement("span");
|
|
838
|
-
preview.className = "ml-preview";
|
|
839
|
-
preview.textContent = ` \u2014 ${msg.preview}`;
|
|
840
|
-
subject.appendChild(preview);
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
const date = document.createElement("span");
|
|
844
|
-
date.className = "ml-date";
|
|
845
|
-
date.textContent = formatDate(msg.date);
|
|
846
|
-
|
|
847
|
-
flag.addEventListener("click", async (e) => {
|
|
848
|
-
e.stopPropagation();
|
|
849
|
-
const isFlagged = row.classList.contains("flagged");
|
|
850
|
-
const currentFlags: string[] = msg.flags || [];
|
|
851
|
-
const newFlags = isFlagged
|
|
852
|
-
? currentFlags.filter((f: string) => f !== "\\Flagged")
|
|
853
|
-
: [...currentFlags, "\\Flagged"];
|
|
854
|
-
try {
|
|
855
|
-
await updateFlags(msgAccountId, msg.uid, newFlags);
|
|
856
|
-
msg.flags = newFlags;
|
|
857
|
-
row.classList.toggle("flagged");
|
|
858
|
-
flag.textContent = row.classList.contains("flagged") ? "\u2605" : "\u2606";
|
|
859
|
-
} catch { /* ignore */ }
|
|
860
|
-
});
|
|
861
|
-
|
|
862
|
-
row.appendChild(avatar);
|
|
863
|
-
row.appendChild(flag);
|
|
864
|
-
row.appendChild(from);
|
|
865
|
-
row.appendChild(date);
|
|
866
|
-
row.appendChild(subject);
|
|
867
|
-
|
|
868
|
-
row.addEventListener("click", (e) => {
|
|
869
|
-
if (touchWasScroll) { touchWasScroll = false; return; }
|
|
870
|
-
// Multi-select mode (entered via long-press on touch): taps toggle
|
|
871
|
-
// rows instead of opening messages. Exit mode when the user taps
|
|
872
|
-
// outside any row or presses Escape (handled at the body level).
|
|
873
|
-
const body = row.parentElement as HTMLElement | null;
|
|
874
|
-
if (body?.classList.contains("multi-select-on")) {
|
|
875
|
-
row.classList.toggle("selected");
|
|
876
|
-
lastClickedRow = row;
|
|
877
|
-
updateBulkBar();
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
if (e.shiftKey) {
|
|
881
|
-
const anchor = resolveShiftAnchor();
|
|
882
|
-
if (anchor) {
|
|
883
|
-
clearSelection();
|
|
884
|
-
selectRange(anchor, row);
|
|
885
|
-
lastClickedRow = row;
|
|
886
|
-
row.classList.remove("unread");
|
|
887
|
-
focusMessage(msgAccountId, msg);
|
|
888
|
-
} else {
|
|
889
|
-
// No anchor available (first click of the session, or list
|
|
890
|
-
// was just rebuilt with no selection). Treat as a plain
|
|
891
|
-
// click so the user gets visible feedback rather than a
|
|
892
|
-
// silent no-op.
|
|
893
|
-
clearSelection();
|
|
894
|
-
focusRow(row, msgAccountId, msg);
|
|
895
|
-
lastClickedRow = row;
|
|
896
|
-
row.classList.remove("unread");
|
|
897
|
-
}
|
|
898
|
-
} else if (e.ctrlKey || e.metaKey) {
|
|
899
|
-
row.classList.toggle("selected");
|
|
900
|
-
lastClickedRow = row;
|
|
901
|
-
} else {
|
|
902
|
-
// Atomic unfocus-previous + focus-this.
|
|
903
|
-
clearSelection();
|
|
904
|
-
focusRow(row, msgAccountId, msg);
|
|
905
|
-
lastClickedRow = row;
|
|
906
|
-
row.classList.remove("unread");
|
|
907
|
-
}
|
|
908
|
-
updateBulkBar();
|
|
909
|
-
});
|
|
910
|
-
|
|
911
|
-
// Q64: double-click → pop out the message in a floating overlay so
|
|
912
|
-
// the user can read it without losing the selected list context.
|
|
913
|
-
row.addEventListener("dblclick", (e) => {
|
|
914
|
-
e.preventDefault();
|
|
915
|
-
e.stopPropagation();
|
|
916
|
-
document.dispatchEvent(new CustomEvent("mailx-popout-message", {
|
|
917
|
-
detail: { accountId: msgAccountId, uid: msg.uid, folderId: msg.folderId, subject: msg.subject }
|
|
918
|
-
}));
|
|
919
|
-
});
|
|
920
|
-
|
|
921
|
-
row.addEventListener("dragstart", (e) => {
|
|
922
|
-
if (!row.classList.contains("selected")) {
|
|
923
|
-
clearSelection();
|
|
924
|
-
row.classList.add("selected");
|
|
925
|
-
lastClickedRow = row;
|
|
926
|
-
}
|
|
927
|
-
const selected = getSelectedMessages();
|
|
928
|
-
e.dataTransfer!.setData("application/x-mailx-messages", JSON.stringify(selected));
|
|
929
|
-
e.dataTransfer!.setData("application/x-mailx-message", JSON.stringify({
|
|
930
|
-
accountId: msgAccountId,
|
|
931
|
-
uid: msg.uid,
|
|
932
|
-
folderId: msg.folderId,
|
|
933
|
-
subject: msg.subject,
|
|
934
|
-
}));
|
|
935
|
-
e.dataTransfer!.effectAllowed = "copyMove";
|
|
936
|
-
row.classList.add("dragging");
|
|
937
|
-
if (selected.length > 1) {
|
|
938
|
-
const badge = document.createElement("div");
|
|
939
|
-
badge.textContent = `${selected.length} messages`;
|
|
940
|
-
badge.style.cssText = "position:absolute;top:-1000px;background:#333;color:white;padding:4px 8px;border-radius:4px;font-size:12px";
|
|
941
|
-
document.body.appendChild(badge);
|
|
942
|
-
e.dataTransfer!.setDragImage(badge, 0, 0);
|
|
943
|
-
setTimeout(() => badge.remove(), 0);
|
|
944
|
-
}
|
|
945
|
-
});
|
|
946
|
-
row.addEventListener("dragend", () => row.classList.remove("dragging"));
|
|
947
|
-
|
|
948
|
-
// ── Q66: long-press on touch → context menu ──
|
|
949
|
-
// Mirrors right-click on the phone where right-click isn't a thing.
|
|
950
|
-
// Cancelled by any touchmove or touchend before the threshold.
|
|
951
|
-
let longPressTimer: ReturnType<typeof setTimeout> | null = null;
|
|
952
|
-
const LONG_PRESS_MS = 550;
|
|
953
|
-
row.addEventListener("touchstart", (e: TouchEvent) => {
|
|
954
|
-
const t = e.touches[0];
|
|
955
|
-
if (!t) return;
|
|
956
|
-
const cx = t.clientX, cy = t.clientY;
|
|
957
|
-
if (longPressTimer) clearTimeout(longPressTimer);
|
|
958
|
-
longPressTimer = setTimeout(() => {
|
|
959
|
-
longPressTimer = null;
|
|
960
|
-
// Long-press semantics:
|
|
961
|
-
// - If the list is already in multi-select mode, toggle this
|
|
962
|
-
// row's selected state (so the user can extend a selection
|
|
963
|
-
// without needing a second long-press-and-menu dance).
|
|
964
|
-
// - Otherwise enter multi-select mode: mark THIS row selected
|
|
965
|
-
// and add a sticky class on the body so future taps toggle
|
|
966
|
-
// instead of opening messages. Tap elsewhere or press
|
|
967
|
-
// Escape to exit.
|
|
968
|
-
const body = row.parentElement as HTMLElement | null;
|
|
969
|
-
const alreadyMulti = body?.classList.contains("multi-select-on");
|
|
970
|
-
if (alreadyMulti) {
|
|
971
|
-
row.classList.toggle("selected");
|
|
972
|
-
} else {
|
|
973
|
-
clearSelection();
|
|
974
|
-
row.classList.add("selected");
|
|
975
|
-
body?.classList.add("multi-select-on");
|
|
976
|
-
}
|
|
977
|
-
lastClickedRow = row;
|
|
978
|
-
updateBulkBar();
|
|
979
|
-
// Haptic hint if the platform supports it (Android WebView does).
|
|
980
|
-
try { (navigator as any).vibrate?.(20); } catch { /* */ }
|
|
981
|
-
}, LONG_PRESS_MS);
|
|
982
|
-
}, { passive: true });
|
|
983
|
-
const cancelLongPress = () => {
|
|
984
|
-
if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; }
|
|
985
|
-
};
|
|
986
|
-
row.addEventListener("touchmove", cancelLongPress, { passive: true });
|
|
987
|
-
row.addEventListener("touchend", cancelLongPress, { passive: true });
|
|
988
|
-
row.addEventListener("touchcancel", cancelLongPress, { passive: true });
|
|
989
|
-
|
|
990
|
-
// ── Right-click context menu ──
|
|
991
|
-
row.addEventListener("contextmenu", (e) => {
|
|
992
|
-
e.preventDefault();
|
|
993
|
-
// Selection-on-context-click semantics:
|
|
994
|
-
// - If the right-clicked row is already selected → keep the
|
|
995
|
-
// existing selection (single or multi). The menu acts on
|
|
996
|
-
// whatever is selected.
|
|
997
|
-
// - If it's NOT selected and we're in multi-select mode →
|
|
998
|
-
// ADD this row to the selection (don't clear). Long-press
|
|
999
|
-
// to open a context menu used to wipe the multi-select set,
|
|
1000
|
-
// which was the user-reported "annoying" behavior.
|
|
1001
|
-
// - If it's NOT selected and we're NOT in multi-select →
|
|
1002
|
-
// single-select this row (replace prior selection).
|
|
1003
|
-
const body = row.parentElement as HTMLElement | null;
|
|
1004
|
-
const inMulti = !!body?.classList.contains("multi-select-on");
|
|
1005
|
-
if (!row.classList.contains("selected")) {
|
|
1006
|
-
if (inMulti) {
|
|
1007
|
-
row.classList.add("selected");
|
|
1008
|
-
lastClickedRow = row;
|
|
1009
|
-
} else {
|
|
1010
|
-
clearSelection();
|
|
1011
|
-
row.classList.add("selected");
|
|
1012
|
-
lastClickedRow = row;
|
|
1013
|
-
focusMessage(msgAccountId, msg);
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
const isSeen = msg.flags.includes("\\Seen");
|
|
1018
|
-
const isFlagged = msg.flags.includes("\\Flagged");
|
|
1019
|
-
|
|
1020
|
-
const items: MenuItem[] = [
|
|
1021
|
-
{
|
|
1022
|
-
label: isSeen ? "Mark unread" : "Mark read",
|
|
1023
|
-
action: async () => {
|
|
1024
|
-
const newFlags = isSeen
|
|
1025
|
-
? msg.flags.filter((f: string) => f !== "\\Seen")
|
|
1026
|
-
: [...msg.flags, "\\Seen"];
|
|
1027
|
-
try {
|
|
1028
|
-
await updateFlags(msgAccountId, msg.uid, newFlags);
|
|
1029
|
-
msg.flags = newFlags;
|
|
1030
|
-
state.updateMessageFlags(msgAccountId, msg.uid, newFlags);
|
|
1031
|
-
row.classList.toggle("unread", !newFlags.includes("\\Seen"));
|
|
1032
|
-
} catch { /* ignore */ }
|
|
1033
|
-
},
|
|
1034
|
-
},
|
|
1035
|
-
{
|
|
1036
|
-
label: isFlagged ? "Unflag" : "Flag",
|
|
1037
|
-
action: async () => {
|
|
1038
|
-
const newFlags = isFlagged
|
|
1039
|
-
? msg.flags.filter((f: string) => f !== "\\Flagged")
|
|
1040
|
-
: [...msg.flags, "\\Flagged"];
|
|
1041
|
-
try {
|
|
1042
|
-
await updateFlags(msgAccountId, msg.uid, newFlags);
|
|
1043
|
-
msg.flags = newFlags;
|
|
1044
|
-
row.classList.toggle("flagged");
|
|
1045
|
-
flag.textContent = row.classList.contains("flagged") ? "\u2605" : "\u2606";
|
|
1046
|
-
} catch { /* ignore */ }
|
|
1047
|
-
},
|
|
1048
|
-
},
|
|
1049
|
-
{ label: "", action: () => {}, separator: true },
|
|
1050
|
-
{
|
|
1051
|
-
label: "Reply",
|
|
1052
|
-
action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "reply" } })),
|
|
1053
|
-
},
|
|
1054
|
-
{
|
|
1055
|
-
label: "Reply All",
|
|
1056
|
-
action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "replyAll" } })),
|
|
1057
|
-
},
|
|
1058
|
-
{
|
|
1059
|
-
label: "Forward",
|
|
1060
|
-
action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "forward" } })),
|
|
1061
|
-
},
|
|
1062
|
-
{ label: "", action: () => {}, separator: true },
|
|
1063
|
-
{
|
|
1064
|
-
label: "Move to folder…",
|
|
1065
|
-
action: async () => {
|
|
1066
|
-
// Move all currently-selected rows (or just this one if it's the only selection)
|
|
1067
|
-
const selectedRows = Array.from(document.querySelectorAll(".ml-row.selected"));
|
|
1068
|
-
const uids = selectedRows.length > 0
|
|
1069
|
-
? selectedRows.map((r: Element) => Number((r as HTMLElement).dataset.uid)).filter(u => !isNaN(u))
|
|
1070
|
-
: [msg.uid];
|
|
1071
|
-
const pick = await pickFolder(msgAccountId, { excludeFolderIds: [msg.folderId] });
|
|
1072
|
-
if (!pick) return;
|
|
1073
|
-
try {
|
|
1074
|
-
await apiMoveMessages(msgAccountId, uids, pick.folderId);
|
|
1075
|
-
// Remove from local state — reconciler handles server sync.
|
|
1076
|
-
state.removeMessages(uids.map(u => ({ accountId: msgAccountId, uid: u })));
|
|
1077
|
-
} catch (err: any) {
|
|
1078
|
-
alert(`Move failed: ${err.message}`);
|
|
1079
|
-
}
|
|
1080
|
-
},
|
|
1081
|
-
},
|
|
1082
|
-
{
|
|
1083
|
-
label: "Delete",
|
|
1084
|
-
action: () => document.dispatchEvent(new CustomEvent("mailx-delete")),
|
|
1085
|
-
},
|
|
1086
|
-
{ label: "", action: () => {}, separator: true },
|
|
1087
|
-
{
|
|
1088
|
-
label: "⚠ Mark as spam",
|
|
1089
|
-
action: () => document.getElementById("btn-spam")?.click(),
|
|
1090
|
-
},
|
|
1091
|
-
{ label: "", action: () => {}, separator: true },
|
|
1092
|
-
{
|
|
1093
|
-
label: "Copy Message-ID",
|
|
1094
|
-
action: async () => {
|
|
1095
|
-
// Useful when asking "where did my letter go?" — pair the
|
|
1096
|
-
// Message-ID with the reconcile-delete log line.
|
|
1097
|
-
if (!msg.messageId) { alert("No Message-ID on this row."); return; }
|
|
1098
|
-
try { await navigator.clipboard.writeText(msg.messageId); } catch { /* */ }
|
|
1099
|
-
},
|
|
1100
|
-
},
|
|
1101
|
-
];
|
|
1102
|
-
|
|
1103
|
-
showContextMenu(e.clientX, e.clientY, items);
|
|
1104
|
-
});
|
|
1105
|
-
|
|
1106
|
-
body.appendChild(row);
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
function formatDate(epochMs: number): string {
|
|
1111
|
-
const d = new Date(epochMs);
|
|
1112
|
-
const now = new Date();
|
|
1113
|
-
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1114
|
-
const msgDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
1115
|
-
|
|
1116
|
-
if (msgDay.getTime() === today.getTime())
|
|
1117
|
-
return d.toLocaleTimeString(undefined, timeFmt);
|
|
1118
|
-
|
|
1119
|
-
if (d.getFullYear() === now.getFullYear())
|
|
1120
|
-
return d.toLocaleString(undefined, dateFmtSameYear);
|
|
1121
|
-
|
|
1122
|
-
return d.toLocaleString(undefined, dateFmt);
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
function escapeHtml(s: string): string {
|
|
1126
|
-
const div = document.createElement("div");
|
|
1127
|
-
div.textContent = s;
|
|
1128
|
-
return div.innerHTML;
|
|
1129
|
-
}
|