@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
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { getMessages as apiGetMessages, getUnifiedInbox as apiGetUnifiedInbox, searchMessages, updateFlags, getThreadMessages, moveMessages as apiMoveMessages } from "../lib/api-client.js";
|
|
6
6
|
import * as state from "../lib/message-state.js";
|
|
7
|
+
import { showMessage as viewerShow, clearViewer as viewerClear } from "./message-viewer.js";
|
|
7
8
|
import { showContextMenu } from "./context-menu.js";
|
|
8
9
|
import { pickFolder } from "./folder-picker.js";
|
|
9
10
|
let onMessageSelect;
|
|
@@ -25,46 +26,60 @@ let touchWasScroll = false;
|
|
|
25
26
|
// (text columns default asc, date defaults desc).
|
|
26
27
|
let currentSort = "date";
|
|
27
28
|
let currentSortDir = "desc";
|
|
28
|
-
/**
|
|
29
|
+
/** Single source of truth for "which row is focused" in the list.
|
|
29
30
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
31
|
+
* Each rendered row is a `MessageRow` instance owning its DOM element,
|
|
32
|
+
* its message envelope, and its event handlers. `focusRow(row)` runs
|
|
33
|
+
* the atomic transition: unfocus the previous row, mark this one
|
|
34
|
+
* `.selected`, drive the viewer with the row's envelope, dispatch
|
|
35
|
+
* `mailx-focus-changed`. There is no "select state" anywhere else; the
|
|
36
|
+
* viewer has no subscriptions. If `focusRow` isn't called, the preview
|
|
37
|
+
* pane doesn't update — drift between the highlighted row and the
|
|
38
|
+
* preview is structurally impossible.
|
|
32
39
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
40
|
+
* When the focused row's data leaves the list (delete, move, search
|
|
41
|
+
* reload, folder switch), the controller hands focus to a survivor
|
|
42
|
+
* via `focusByIdentity` or, if no survivor exists, calls
|
|
43
|
+
* `releaseFocus()` which clears highlight + viewer in the same call. */
|
|
44
|
+
let focusedRow = null;
|
|
45
|
+
const rowByKey = new Map();
|
|
46
|
+
function rowKey(accountId, uid) {
|
|
47
|
+
return `${accountId}:${uid}`;
|
|
48
|
+
}
|
|
49
|
+
function focusRow(row) {
|
|
50
|
+
if (focusedRow && focusedRow !== row)
|
|
51
|
+
focusedRow.setSelected(false);
|
|
52
|
+
row.setSelected(true);
|
|
53
|
+
focusedRow = row;
|
|
54
|
+
// Drive the viewer with the row's own envelope. Single call site;
|
|
55
|
+
// the viewer paints headers immediately, fetches body in background.
|
|
56
|
+
viewerShow(row.accountId, row.msg.uid, row.msg.folderId, undefined, false, row.msg);
|
|
57
|
+
onMessageSelect(row.accountId, row.msg.uid, row.msg.folderId);
|
|
58
|
+
document.dispatchEvent(new CustomEvent("mailx-focus-changed", { detail: row.msg }));
|
|
59
|
+
}
|
|
60
|
+
/** Read the currently-focused message envelope. Used by app-level
|
|
61
|
+
* features (flag toggle, mark unread, status bar) that need to know
|
|
62
|
+
* what's open in the viewer. */
|
|
63
|
+
export function getCurrentFocused() {
|
|
64
|
+
return focusedRow ? focusedRow.msg : null;
|
|
56
65
|
}
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
/** Programmatic focus by identity. Used for thread-popup clicks,
|
|
67
|
+
* keyboard nav, post-delete handoff. Returns true if a row was found
|
|
68
|
+
* and focused. */
|
|
69
|
+
function focusByIdentity(accountId, uid) {
|
|
70
|
+
const row = rowByKey.get(rowKey(accountId, uid));
|
|
71
|
+
if (!row)
|
|
72
|
+
return false;
|
|
73
|
+
focusRow(row);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
/** Release the focus slot and clear the preview pane in one call. */
|
|
77
|
+
export function releaseFocus() {
|
|
78
|
+
if (focusedRow)
|
|
79
|
+
focusedRow.setSelected(false);
|
|
80
|
+
focusedRow = null;
|
|
81
|
+
viewerClear();
|
|
82
|
+
document.dispatchEvent(new CustomEvent("mailx-focus-changed", { detail: null }));
|
|
68
83
|
}
|
|
69
84
|
/** Flip the "not-downloaded" indicator off for rows whose bodies just cached.
|
|
70
85
|
* Called from the bodyCached service event — covers both background prefetch
|
|
@@ -96,10 +111,14 @@ function clearSelection() {
|
|
|
96
111
|
const body = document.getElementById("ml-body");
|
|
97
112
|
if (body)
|
|
98
113
|
body.querySelectorAll(".ml-row.selected").forEach(r => r.classList.remove("selected"));
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
114
|
+
// The focused-row invariant is "the Row whose .selected is currently
|
|
115
|
+
// mine". clearSelection wipes all .selected, so the invariant breaks
|
|
116
|
+
// unless we drop the focused-row reference too.
|
|
117
|
+
if (focusedRow) {
|
|
118
|
+
focusedRow = null;
|
|
119
|
+
viewerClear();
|
|
120
|
+
document.dispatchEvent(new CustomEvent("mailx-focus-changed", { detail: null }));
|
|
121
|
+
}
|
|
103
122
|
}
|
|
104
123
|
/** Deterministic sender-avatar color from a seed string (typically the
|
|
105
124
|
* email address). Hash → hue at 12 evenly-spaced positions on the wheel.
|
|
@@ -238,10 +257,14 @@ export function initMessageList(handler) {
|
|
|
238
257
|
}
|
|
239
258
|
});
|
|
240
259
|
}
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
260
|
+
// Viewer signals "this row is gone server-side" via mailx-remove-stale.
|
|
261
|
+
// The list owns row lifecycle, so it runs the removal here — filtering
|
|
262
|
+
// state, removing the DOM row, and handing focus to a survivor (or
|
|
263
|
+
// clearing the pane) in one transaction.
|
|
264
|
+
document.addEventListener("mailx-remove-stale", (e) => {
|
|
265
|
+
const { accountId, uid } = e.detail || {};
|
|
266
|
+
if (typeof uid === "number" && typeof accountId === "string") {
|
|
267
|
+
removeMessagesAndReconcile([{ accountId, uid }]);
|
|
245
268
|
}
|
|
246
269
|
});
|
|
247
270
|
// Sort column headers — click to cycle. Date defaults desc (newest first);
|
|
@@ -311,39 +334,46 @@ function updateSortIndicators() {
|
|
|
311
334
|
});
|
|
312
335
|
}
|
|
313
336
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
337
|
+
* Remove the named messages from the list and reconcile DOM + focus.
|
|
338
|
+
*
|
|
339
|
+
* Single-transaction transition: filters the underlying state, deletes
|
|
340
|
+
* the DOM rows, and either re-focuses a surviving row or releases focus
|
|
341
|
+
* (clearing the viewer). Replaces the old subscribe-and-sync model where
|
|
342
|
+
* state.removeMessages broadcast a "removed" event to two independent
|
|
343
|
+
* subscribers — that path could leave the highlight and preview out of
|
|
344
|
+
* sync if the list and viewer noticed the change in different orders.
|
|
345
|
+
*
|
|
346
|
+
* Call this whenever local rows need to disappear (delete, move,
|
|
347
|
+
* server-side stale removal, undo). Pass identities; the function
|
|
348
|
+
* decides what to focus next.
|
|
316
349
|
*/
|
|
317
|
-
function
|
|
350
|
+
export function removeMessagesAndReconcile(uids) {
|
|
351
|
+
const focusedIdent = focusedRow
|
|
352
|
+
? { accountId: focusedRow.accountId, uid: focusedRow.msg.uid }
|
|
353
|
+
: null;
|
|
354
|
+
const outcome = state.removeMessages(uids, focusedIdent);
|
|
318
355
|
const body = document.getElementById("ml-body");
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const key = `${el.dataset.accountId}:${el.dataset.uid}`;
|
|
327
|
-
if (!stateUids.has(key)) {
|
|
328
|
-
el.remove();
|
|
356
|
+
if (body) {
|
|
357
|
+
const stateUids = new Set(state.getMessages().map(m => `${m.accountId}:${m.uid}`));
|
|
358
|
+
for (const row of Array.from(body.querySelectorAll(".ml-row"))) {
|
|
359
|
+
const el = row;
|
|
360
|
+
const key = `${el.dataset.accountId}:${el.dataset.uid}`;
|
|
361
|
+
if (!stateUids.has(key))
|
|
362
|
+
el.remove();
|
|
329
363
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
clearSelection();
|
|
333
|
-
const sel = state.getSelected();
|
|
334
|
-
if (sel) {
|
|
335
|
-
const row = body.querySelector(`.ml-row[data-uid="${sel.uid}"][data-account-id="${sel.accountId}"]`)
|
|
336
|
-
|| body.querySelector(`.ml-row[data-uid="${sel.uid}"]`);
|
|
337
|
-
if (row) {
|
|
338
|
-
row.classList.add("selected");
|
|
339
|
-
lastClickedRow = row;
|
|
340
|
-
// Trigger viewer update
|
|
341
|
-
onMessageSelect(sel.accountId, sel.uid, sel.folderId);
|
|
364
|
+
if (state.getMessages().length === 0) {
|
|
365
|
+
body.innerHTML = `<div class="ml-empty">No messages</div>`;
|
|
342
366
|
}
|
|
343
367
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
368
|
+
if (outcome.focusedWasRemoved) {
|
|
369
|
+
if (outcome.nextSurvivor && focusByIdentity(outcome.nextSurvivor.accountId, outcome.nextSurvivor.uid)) {
|
|
370
|
+
// focusByIdentity handled the transition (DOM class + viewer).
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
// No survivor (or its row didn't render) — release focus and
|
|
374
|
+
// clear the pane so there's no orphaned preview.
|
|
375
|
+
releaseFocus();
|
|
376
|
+
}
|
|
347
377
|
}
|
|
348
378
|
}
|
|
349
379
|
/** Reload the currently displayed folder (preserves current selection) */
|
|
@@ -418,11 +448,11 @@ export async function loadSearchResults(query, scope = "all", accountId = "", fo
|
|
|
418
448
|
const body = document.getElementById("ml-body");
|
|
419
449
|
if (!body)
|
|
420
450
|
return;
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
|
|
451
|
+
// Search reload tears down the current row set — focus must release
|
|
452
|
+
// along with the rows. releaseFocus clears the preview pane in the
|
|
453
|
+
// same call frame, so no orphan preview lingers behind a list that
|
|
454
|
+
// no longer contains the previously-shown row.
|
|
455
|
+
releaseFocus();
|
|
426
456
|
body.innerHTML = `<div class="ml-empty">Searching...</div>`;
|
|
427
457
|
try {
|
|
428
458
|
// Regex search: filter client-side
|
|
@@ -618,7 +648,26 @@ export async function showThreadPopup(pillEl, headMsg) {
|
|
|
618
648
|
item.appendChild(date);
|
|
619
649
|
item.appendChild(subject);
|
|
620
650
|
item.addEventListener("click", async () => {
|
|
621
|
-
|
|
651
|
+
// Thread popup → viewer-only update. The list keeps showing the
|
|
652
|
+
// thread head highlighted (it's the row in the actual list);
|
|
653
|
+
// the viewer pivots to the clicked thread member. Single path
|
|
654
|
+
// in: the viewer's show() call. No state, no event, no drift.
|
|
655
|
+
const envelope = {
|
|
656
|
+
accountId: msg.accountId,
|
|
657
|
+
uid: msg.uid,
|
|
658
|
+
folderId: msg.folderId,
|
|
659
|
+
subject: msg.subject,
|
|
660
|
+
from: msg.from,
|
|
661
|
+
to: msg.to,
|
|
662
|
+
cc: msg.cc,
|
|
663
|
+
date: msg.date,
|
|
664
|
+
flags: msg.flags,
|
|
665
|
+
size: msg.size,
|
|
666
|
+
preview: msg.preview,
|
|
667
|
+
hasAttachments: msg.hasAttachments,
|
|
668
|
+
};
|
|
669
|
+
viewerShow(msg.accountId, msg.uid, msg.folderId, undefined, false, envelope);
|
|
670
|
+
onMessageSelect(msg.accountId, msg.uid, msg.folderId);
|
|
622
671
|
popup.remove();
|
|
623
672
|
});
|
|
624
673
|
popup.appendChild(item);
|
|
@@ -638,36 +687,35 @@ export async function showThreadPopup(pillEl, headMsg) {
|
|
|
638
687
|
document.addEventListener("mousedown", dismiss, true);
|
|
639
688
|
}, 0);
|
|
640
689
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
for (const msg of rowsToRender) {
|
|
669
|
-
const msgAccountId = msg.accountId || accountId;
|
|
690
|
+
/** A rendered row in the message list.
|
|
691
|
+
*
|
|
692
|
+
* Owns its DOM element, the message envelope it represents, and all of
|
|
693
|
+
* its event handlers (click, dblclick, drag, contextmenu, touch
|
|
694
|
+
* long-press, plus the avatar / flag / thread-pill child handlers).
|
|
695
|
+
* State changes that affect appearance (selected, unread, flagged,
|
|
696
|
+
* body-cached) go through methods so the OO layer is the single point
|
|
697
|
+
* of mutation — no `el.classList.toggle("selected")` scattered through
|
|
698
|
+
* the codebase.
|
|
699
|
+
*
|
|
700
|
+
* Lifecycle: constructor builds DOM and wires handlers; `attach(body)`
|
|
701
|
+
* inserts into the list; `detach()` removes from DOM and from the
|
|
702
|
+
* module-level rowByKey map. The list controller diff-updates rows on
|
|
703
|
+
* list mutations.
|
|
704
|
+
*
|
|
705
|
+
* Multi-select still uses `.selected` class queries — that's a single
|
|
706
|
+
* DOM-level concept where drift isn't an issue (no separate render
|
|
707
|
+
* surface to drift against). The Row class wraps the *focus* concept
|
|
708
|
+
* (which couples to the viewer) as a fate-shared unit. */
|
|
709
|
+
class MessageRow {
|
|
710
|
+
msg;
|
|
711
|
+
accountId;
|
|
712
|
+
el;
|
|
713
|
+
flagEl;
|
|
714
|
+
constructor(msg, accountId, threadHead, threadCount, showAccountTag) {
|
|
715
|
+
this.msg = msg;
|
|
716
|
+
this.accountId = accountId;
|
|
670
717
|
const row = document.createElement("div");
|
|
718
|
+
this.el = row;
|
|
671
719
|
row.className = "ml-row";
|
|
672
720
|
row.draggable = true;
|
|
673
721
|
if (!msg.flags.includes("\\Seen"))
|
|
@@ -676,25 +724,18 @@ function appendMessages(body, accountId, items) {
|
|
|
676
724
|
row.classList.add("flagged");
|
|
677
725
|
if (!msg.bodyPath)
|
|
678
726
|
row.classList.add("not-downloaded");
|
|
679
|
-
// Pink-row visible reconciliation state (S1 slice C): a queued local
|
|
680
|
-
// action (move/flag/delete) hasn't been ACK'd by the server yet.
|
|
681
727
|
if (msg.pending)
|
|
682
728
|
row.classList.add("pending-reconcile");
|
|
683
|
-
// Reply-row marker: messages with In-Reply-To are replies. Shows a
|
|
684
|
-
// subtle left-border accent so the eye can pick out threaded replies
|
|
685
|
-
// without enabling full thread grouping.
|
|
686
729
|
if (msg.inReplyTo)
|
|
687
730
|
row.classList.add("is-reply");
|
|
688
731
|
row.dataset.uid = String(msg.uid);
|
|
689
|
-
row.dataset.accountId =
|
|
732
|
+
row.dataset.accountId = accountId;
|
|
690
733
|
row.dataset.folderId = String(msg.folderId);
|
|
691
734
|
if (msg.threadId)
|
|
692
735
|
row.dataset.threadId = msg.threadId;
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
//
|
|
696
|
-
// checkmark via CSS. Color is derived deterministically from the
|
|
697
|
-
// address so the same sender keeps the same color across rows.
|
|
736
|
+
if (threadHead)
|
|
737
|
+
row.classList.add("thread-head");
|
|
738
|
+
// ── Avatar (sender circle, doubles as multi-select affordance) ──
|
|
698
739
|
const fromName = (showToInsteadOfFrom && msg.to?.length)
|
|
699
740
|
? (msg.to[0].name || msg.to[0].address || "?")
|
|
700
741
|
: (msg.from?.name || msg.from?.address || "?");
|
|
@@ -705,81 +746,16 @@ function appendMessages(body, accountId, items) {
|
|
|
705
746
|
avatar.textContent = initial;
|
|
706
747
|
avatar.style.background = senderColor(seedAddr);
|
|
707
748
|
avatar.title = msg.from?.address || "";
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
//
|
|
711
|
-
// avatar a dedicated selection affordance.
|
|
712
|
-
avatar.addEventListener("click", (e) => {
|
|
713
|
-
e.stopPropagation();
|
|
714
|
-
const body = document.getElementById("ml-body");
|
|
715
|
-
if (!body)
|
|
716
|
-
return;
|
|
717
|
-
if (body.classList.contains("multi-select-on")) {
|
|
718
|
-
row.classList.toggle("selected");
|
|
719
|
-
}
|
|
720
|
-
else {
|
|
721
|
-
clearSelection();
|
|
722
|
-
row.classList.add("selected");
|
|
723
|
-
body.classList.add("multi-select-on");
|
|
724
|
-
}
|
|
725
|
-
lastClickedRow = row;
|
|
726
|
-
updateBulkBar();
|
|
727
|
-
});
|
|
728
|
-
// Right-click (or long-press) on the avatar → bulk-selection menu.
|
|
729
|
-
// Putting it on the avatar is contextually right: the avatar is the
|
|
730
|
-
// "select" affordance, so its menu owns operations on the selection
|
|
731
|
-
// set. "Select all visible" is the load-bearing item — there's no
|
|
732
|
-
// Ctrl-A equivalent on touch and the scope-after-search use case
|
|
733
|
-
// demands it.
|
|
734
|
-
avatar.addEventListener("contextmenu", async (e) => {
|
|
735
|
-
e.preventDefault();
|
|
736
|
-
e.stopPropagation();
|
|
737
|
-
const { showContextMenu } = await import("./context-menu.js");
|
|
738
|
-
const body = document.getElementById("ml-body");
|
|
739
|
-
const visibleRows = body
|
|
740
|
-
? Array.from(body.querySelectorAll(".ml-row:not(.filter-hidden)"))
|
|
741
|
-
: [];
|
|
742
|
-
const selectedCount = body
|
|
743
|
-
? body.querySelectorAll(".ml-row.selected").length
|
|
744
|
-
: 0;
|
|
745
|
-
showContextMenu(e.clientX, e.clientY, [
|
|
746
|
-
{
|
|
747
|
-
label: `Select all (${visibleRows.length})`,
|
|
748
|
-
action: () => {
|
|
749
|
-
if (!body)
|
|
750
|
-
return;
|
|
751
|
-
body.classList.add("multi-select-on");
|
|
752
|
-
for (const r of visibleRows)
|
|
753
|
-
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)
|
|
768
|
-
return;
|
|
769
|
-
body.classList.add("multi-select-on");
|
|
770
|
-
for (const r of visibleRows)
|
|
771
|
-
r.classList.toggle("selected");
|
|
772
|
-
lastClickedRow = visibleRows[visibleRows.length - 1] || null;
|
|
773
|
-
updateBulkBar();
|
|
774
|
-
},
|
|
775
|
-
disabled: visibleRows.length === 0,
|
|
776
|
-
},
|
|
777
|
-
]);
|
|
778
|
-
});
|
|
749
|
+
avatar.addEventListener("click", (e) => this.onAvatarClick(e));
|
|
750
|
+
avatar.addEventListener("contextmenu", (e) => this.onAvatarContextMenu(e));
|
|
751
|
+
// ── Flag star (toggle on click) ──
|
|
779
752
|
const flag = document.createElement("span");
|
|
780
753
|
flag.className = "ml-flag";
|
|
781
|
-
flag.textContent = msg.flags.includes("\\Flagged") ? "
|
|
754
|
+
flag.textContent = msg.flags.includes("\\Flagged") ? "★" : "☆";
|
|
782
755
|
flag.title = "Toggle flag";
|
|
756
|
+
flag.addEventListener("click", (e) => this.onFlagClick(e));
|
|
757
|
+
this.flagEl = flag;
|
|
758
|
+
// ── From column ──
|
|
783
759
|
const from = document.createElement("span");
|
|
784
760
|
from.className = "ml-from";
|
|
785
761
|
if (showToInsteadOfFrom && msg.to?.length) {
|
|
@@ -788,15 +764,13 @@ function appendMessages(body, accountId, items) {
|
|
|
788
764
|
else {
|
|
789
765
|
from.textContent = msg.from.name || msg.from.address;
|
|
790
766
|
}
|
|
791
|
-
if (
|
|
767
|
+
if (showAccountTag && accountId) {
|
|
792
768
|
const tag = document.createElement("span");
|
|
793
769
|
tag.className = "ml-account-tag";
|
|
794
|
-
tag.textContent =
|
|
795
|
-
tag.title =
|
|
770
|
+
tag.textContent = accountId.charAt(0).toUpperCase();
|
|
771
|
+
tag.title = accountId;
|
|
796
772
|
from.prepend(tag);
|
|
797
773
|
}
|
|
798
|
-
// Search/cross-folder results carry folderName — show a tag so the user
|
|
799
|
-
// can tell which folder each hit lives in.
|
|
800
774
|
if (msg.folderName) {
|
|
801
775
|
const folderTag = document.createElement("span");
|
|
802
776
|
folderTag.className = "ml-folder-tag";
|
|
@@ -804,8 +778,6 @@ function appendMessages(body, accountId, items) {
|
|
|
804
778
|
folderTag.title = `In folder: ${msg.folderName}`;
|
|
805
779
|
from.prepend(folderTag);
|
|
806
780
|
}
|
|
807
|
-
// Unified inbox: same Message-ID exists under >=2 accounts → ⇆ badge.
|
|
808
|
-
// Tooltip names the count so the user knows "this appears on N".
|
|
809
781
|
if (msg.dupeCount >= 2) {
|
|
810
782
|
const dupe = document.createElement("span");
|
|
811
783
|
dupe.className = "ml-dupe-tag";
|
|
@@ -813,173 +785,231 @@ function appendMessages(body, accountId, items) {
|
|
|
813
785
|
dupe.title = `Same message on ${msg.dupeCount} accounts`;
|
|
814
786
|
from.prepend(dupe);
|
|
815
787
|
}
|
|
788
|
+
// ── Subject (with optional thread pill + preview snippet) ──
|
|
816
789
|
const subject = document.createElement("span");
|
|
817
790
|
subject.className = "ml-subject";
|
|
818
791
|
subject.innerHTML = escapeHtml(msg.subject);
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
threadPill
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
threadPill.addEventListener("click", async (e) => {
|
|
830
|
-
e.stopPropagation();
|
|
831
|
-
await showThreadPopup(threadPill, msg);
|
|
832
|
-
});
|
|
833
|
-
subject.prepend(threadPill);
|
|
834
|
-
}
|
|
792
|
+
if (threadHead && threadCount > 1 && msg.threadId) {
|
|
793
|
+
const threadPill = document.createElement("span");
|
|
794
|
+
threadPill.className = "ml-thread-pill";
|
|
795
|
+
threadPill.textContent = String(threadCount);
|
|
796
|
+
threadPill.title = `${threadCount} messages in this thread — click to see list`;
|
|
797
|
+
threadPill.addEventListener("click", async (e) => {
|
|
798
|
+
e.stopPropagation();
|
|
799
|
+
await showThreadPopup(threadPill, msg);
|
|
800
|
+
});
|
|
801
|
+
subject.prepend(threadPill);
|
|
835
802
|
}
|
|
836
803
|
if (msg.preview) {
|
|
837
804
|
const preview = document.createElement("span");
|
|
838
805
|
preview.className = "ml-preview";
|
|
839
|
-
preview.textContent = `
|
|
806
|
+
preview.textContent = ` — ${msg.preview}`;
|
|
840
807
|
subject.appendChild(preview);
|
|
841
808
|
}
|
|
809
|
+
// ── Date column ──
|
|
842
810
|
const date = document.createElement("span");
|
|
843
811
|
date.className = "ml-date";
|
|
844
812
|
date.textContent = formatDate(msg.date);
|
|
845
|
-
flag.addEventListener("click", async (e) => {
|
|
846
|
-
e.stopPropagation();
|
|
847
|
-
const isFlagged = row.classList.contains("flagged");
|
|
848
|
-
const currentFlags = msg.flags || [];
|
|
849
|
-
const newFlags = isFlagged
|
|
850
|
-
? currentFlags.filter((f) => f !== "\\Flagged")
|
|
851
|
-
: [...currentFlags, "\\Flagged"];
|
|
852
|
-
try {
|
|
853
|
-
await updateFlags(msgAccountId, msg.uid, newFlags);
|
|
854
|
-
msg.flags = newFlags;
|
|
855
|
-
row.classList.toggle("flagged");
|
|
856
|
-
flag.textContent = row.classList.contains("flagged") ? "\u2605" : "\u2606";
|
|
857
|
-
}
|
|
858
|
-
catch { /* ignore */ }
|
|
859
|
-
});
|
|
860
813
|
row.appendChild(avatar);
|
|
861
814
|
row.appendChild(flag);
|
|
862
815
|
row.appendChild(from);
|
|
863
816
|
row.appendChild(date);
|
|
864
817
|
row.appendChild(subject);
|
|
865
|
-
row.addEventListener("click", (e) =>
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
818
|
+
row.addEventListener("click", (e) => this.onRowClick(e));
|
|
819
|
+
row.addEventListener("dblclick", (e) => this.onRowDoubleClick(e));
|
|
820
|
+
row.addEventListener("dragstart", (e) => this.onDragStart(e));
|
|
821
|
+
row.addEventListener("dragend", () => row.classList.remove("dragging"));
|
|
822
|
+
row.addEventListener("contextmenu", (e) => this.onRowContextMenu(e));
|
|
823
|
+
this.wireLongPress(row);
|
|
824
|
+
}
|
|
825
|
+
/** Insert this row into a list body and register it in the lookup map. */
|
|
826
|
+
attach(body) {
|
|
827
|
+
body.appendChild(this.el);
|
|
828
|
+
rowByKey.set(rowKey(this.accountId, this.msg.uid), this);
|
|
829
|
+
}
|
|
830
|
+
/** Remove this row from the DOM and the lookup map. If it was the
|
|
831
|
+
* focused row, the controller is responsible for releasing focus
|
|
832
|
+
* (this method doesn't auto-clear the viewer because the controller
|
|
833
|
+
* may want to hand focus to a sibling instead). */
|
|
834
|
+
detach() {
|
|
835
|
+
this.el.remove();
|
|
836
|
+
rowByKey.delete(rowKey(this.accountId, this.msg.uid));
|
|
837
|
+
}
|
|
838
|
+
setSelected(yes) { this.el.classList.toggle("selected", yes); }
|
|
839
|
+
get isSelected() { return this.el.classList.contains("selected"); }
|
|
840
|
+
setUnreadClass(yes) { this.el.classList.toggle("unread", yes); }
|
|
841
|
+
setFlaggedClass(yes) {
|
|
842
|
+
this.el.classList.toggle("flagged", yes);
|
|
843
|
+
this.flagEl.textContent = yes ? "★" : "☆";
|
|
844
|
+
}
|
|
845
|
+
markBodyCached() { this.el.classList.remove("not-downloaded"); }
|
|
846
|
+
onAvatarClick(e) {
|
|
847
|
+
e.stopPropagation();
|
|
848
|
+
const body = document.getElementById("ml-body");
|
|
849
|
+
if (!body)
|
|
850
|
+
return;
|
|
851
|
+
if (body.classList.contains("multi-select-on")) {
|
|
852
|
+
this.setSelected(!this.isSelected);
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
clearSelection();
|
|
856
|
+
this.setSelected(true);
|
|
857
|
+
body.classList.add("multi-select-on");
|
|
858
|
+
}
|
|
859
|
+
lastClickedRow = this.el;
|
|
860
|
+
updateBulkBar();
|
|
861
|
+
}
|
|
862
|
+
async onAvatarContextMenu(e) {
|
|
863
|
+
e.preventDefault();
|
|
864
|
+
e.stopPropagation();
|
|
865
|
+
const { showContextMenu: showMenu } = await import("./context-menu.js");
|
|
866
|
+
const body = document.getElementById("ml-body");
|
|
867
|
+
const visibleRows = body
|
|
868
|
+
? Array.from(body.querySelectorAll(".ml-row:not(.filter-hidden)"))
|
|
869
|
+
: [];
|
|
870
|
+
const selectedCount = body
|
|
871
|
+
? body.querySelectorAll(".ml-row.selected").length
|
|
872
|
+
: 0;
|
|
873
|
+
showMenu(e.clientX, e.clientY, [
|
|
874
|
+
{
|
|
875
|
+
label: `Select all (${visibleRows.length})`,
|
|
876
|
+
action: () => {
|
|
877
|
+
if (!body)
|
|
878
|
+
return;
|
|
879
|
+
body.classList.add("multi-select-on");
|
|
880
|
+
for (const r of visibleRows)
|
|
881
|
+
r.classList.add("selected");
|
|
882
|
+
lastClickedRow = visibleRows[visibleRows.length - 1] || null;
|
|
883
|
+
updateBulkBar();
|
|
884
|
+
},
|
|
885
|
+
disabled: visibleRows.length === 0,
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
label: `Clear selection${selectedCount ? ` (${selectedCount})` : ""}`,
|
|
889
|
+
action: () => exitMultiSelect(),
|
|
890
|
+
disabled: selectedCount === 0,
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
label: "Invert selection",
|
|
894
|
+
action: () => {
|
|
895
|
+
if (!body)
|
|
896
|
+
return;
|
|
897
|
+
body.classList.add("multi-select-on");
|
|
898
|
+
for (const r of visibleRows)
|
|
899
|
+
r.classList.toggle("selected");
|
|
900
|
+
lastClickedRow = visibleRows[visibleRows.length - 1] || null;
|
|
901
|
+
updateBulkBar();
|
|
902
|
+
},
|
|
903
|
+
disabled: visibleRows.length === 0,
|
|
904
|
+
},
|
|
905
|
+
]);
|
|
906
|
+
}
|
|
907
|
+
async onFlagClick(e) {
|
|
908
|
+
e.stopPropagation();
|
|
909
|
+
const isFlagged = this.el.classList.contains("flagged");
|
|
910
|
+
const currentFlags = this.msg.flags || [];
|
|
911
|
+
const newFlags = isFlagged
|
|
912
|
+
? currentFlags.filter((f) => f !== "\\Flagged")
|
|
913
|
+
: [...currentFlags, "\\Flagged"];
|
|
914
|
+
try {
|
|
915
|
+
await updateFlags(this.accountId, this.msg.uid, newFlags);
|
|
916
|
+
this.msg.flags = newFlags;
|
|
917
|
+
this.setFlaggedClass(!isFlagged);
|
|
918
|
+
}
|
|
919
|
+
catch { /* ignore */ }
|
|
920
|
+
}
|
|
921
|
+
onRowClick(e) {
|
|
922
|
+
if (touchWasScroll) {
|
|
923
|
+
touchWasScroll = false;
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
const body = this.el.parentElement;
|
|
927
|
+
if (body?.classList.contains("multi-select-on")) {
|
|
928
|
+
this.setSelected(!this.isSelected);
|
|
929
|
+
lastClickedRow = this.el;
|
|
911
930
|
updateBulkBar();
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
e.stopPropagation();
|
|
918
|
-
document.dispatchEvent(new CustomEvent("mailx-popout-message", {
|
|
919
|
-
detail: { accountId: msgAccountId, uid: msg.uid, folderId: msg.folderId, subject: msg.subject }
|
|
920
|
-
}));
|
|
921
|
-
});
|
|
922
|
-
row.addEventListener("dragstart", (e) => {
|
|
923
|
-
if (!row.classList.contains("selected")) {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (e.shiftKey) {
|
|
934
|
+
const anchor = resolveShiftAnchor();
|
|
935
|
+
if (anchor) {
|
|
924
936
|
clearSelection();
|
|
925
|
-
|
|
926
|
-
lastClickedRow =
|
|
937
|
+
selectRange(anchor, this.el);
|
|
938
|
+
lastClickedRow = this.el;
|
|
939
|
+
this.setUnreadClass(false);
|
|
940
|
+
focusRow(this);
|
|
927
941
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
folderId: msg.folderId,
|
|
934
|
-
subject: msg.subject,
|
|
935
|
-
}));
|
|
936
|
-
e.dataTransfer.effectAllowed = "copyMove";
|
|
937
|
-
row.classList.add("dragging");
|
|
938
|
-
if (selected.length > 1) {
|
|
939
|
-
const badge = document.createElement("div");
|
|
940
|
-
badge.textContent = `${selected.length} messages`;
|
|
941
|
-
badge.style.cssText = "position:absolute;top:-1000px;background:#333;color:white;padding:4px 8px;border-radius:4px;font-size:12px";
|
|
942
|
-
document.body.appendChild(badge);
|
|
943
|
-
e.dataTransfer.setDragImage(badge, 0, 0);
|
|
944
|
-
setTimeout(() => badge.remove(), 0);
|
|
942
|
+
else {
|
|
943
|
+
clearSelection();
|
|
944
|
+
focusRow(this);
|
|
945
|
+
lastClickedRow = this.el;
|
|
946
|
+
this.setUnreadClass(false);
|
|
945
947
|
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
948
|
+
}
|
|
949
|
+
else if (e.ctrlKey || e.metaKey) {
|
|
950
|
+
this.setSelected(!this.isSelected);
|
|
951
|
+
lastClickedRow = this.el;
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
clearSelection();
|
|
955
|
+
focusRow(this);
|
|
956
|
+
lastClickedRow = this.el;
|
|
957
|
+
this.setUnreadClass(false);
|
|
958
|
+
}
|
|
959
|
+
updateBulkBar();
|
|
960
|
+
}
|
|
961
|
+
onRowDoubleClick(e) {
|
|
962
|
+
e.preventDefault();
|
|
963
|
+
e.stopPropagation();
|
|
964
|
+
document.dispatchEvent(new CustomEvent("mailx-popout-message", {
|
|
965
|
+
detail: { accountId: this.accountId, uid: this.msg.uid, folderId: this.msg.folderId, subject: this.msg.subject },
|
|
966
|
+
}));
|
|
967
|
+
}
|
|
968
|
+
onDragStart(e) {
|
|
969
|
+
if (!this.isSelected) {
|
|
970
|
+
clearSelection();
|
|
971
|
+
this.setSelected(true);
|
|
972
|
+
lastClickedRow = this.el;
|
|
973
|
+
}
|
|
974
|
+
const selected = getSelectedMessages();
|
|
975
|
+
e.dataTransfer.setData("application/x-mailx-messages", JSON.stringify(selected));
|
|
976
|
+
e.dataTransfer.setData("application/x-mailx-message", JSON.stringify({
|
|
977
|
+
accountId: this.accountId,
|
|
978
|
+
uid: this.msg.uid,
|
|
979
|
+
folderId: this.msg.folderId,
|
|
980
|
+
subject: this.msg.subject,
|
|
981
|
+
}));
|
|
982
|
+
e.dataTransfer.effectAllowed = "copyMove";
|
|
983
|
+
this.el.classList.add("dragging");
|
|
984
|
+
if (selected.length > 1) {
|
|
985
|
+
const badge = document.createElement("div");
|
|
986
|
+
badge.textContent = `${selected.length} messages`;
|
|
987
|
+
badge.style.cssText = "position:absolute;top:-1000px;background:#333;color:white;padding:4px 8px;border-radius:4px;font-size:12px";
|
|
988
|
+
document.body.appendChild(badge);
|
|
989
|
+
e.dataTransfer.setDragImage(badge, 0, 0);
|
|
990
|
+
setTimeout(() => badge.remove(), 0);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
wireLongPress(row) {
|
|
951
994
|
let longPressTimer = null;
|
|
952
995
|
const LONG_PRESS_MS = 550;
|
|
953
|
-
row.addEventListener("touchstart", (
|
|
954
|
-
const t = e.touches[0];
|
|
955
|
-
if (!t)
|
|
956
|
-
return;
|
|
957
|
-
const cx = t.clientX, cy = t.clientY;
|
|
996
|
+
row.addEventListener("touchstart", (_e) => {
|
|
958
997
|
if (longPressTimer)
|
|
959
998
|
clearTimeout(longPressTimer);
|
|
960
999
|
longPressTimer = setTimeout(() => {
|
|
961
1000
|
longPressTimer = null;
|
|
962
|
-
// Long-press semantics:
|
|
963
|
-
// - If the list is already in multi-select mode, toggle this
|
|
964
|
-
// row's selected state (so the user can extend a selection
|
|
965
|
-
// without needing a second long-press-and-menu dance).
|
|
966
|
-
// - Otherwise enter multi-select mode: mark THIS row selected
|
|
967
|
-
// and add a sticky class on the body so future taps toggle
|
|
968
|
-
// instead of opening messages. Tap elsewhere or press
|
|
969
|
-
// Escape to exit.
|
|
970
1001
|
const body = row.parentElement;
|
|
971
1002
|
const alreadyMulti = body?.classList.contains("multi-select-on");
|
|
972
1003
|
if (alreadyMulti) {
|
|
973
|
-
|
|
1004
|
+
this.setSelected(!this.isSelected);
|
|
974
1005
|
}
|
|
975
1006
|
else {
|
|
976
1007
|
clearSelection();
|
|
977
|
-
|
|
1008
|
+
this.setSelected(true);
|
|
978
1009
|
body?.classList.add("multi-select-on");
|
|
979
1010
|
}
|
|
980
|
-
lastClickedRow =
|
|
1011
|
+
lastClickedRow = this.el;
|
|
981
1012
|
updateBulkBar();
|
|
982
|
-
// Haptic hint if the platform supports it (Android WebView does).
|
|
983
1013
|
try {
|
|
984
1014
|
navigator.vibrate?.(20);
|
|
985
1015
|
}
|
|
@@ -995,130 +1025,137 @@ function appendMessages(body, accountId, items) {
|
|
|
995
1025
|
row.addEventListener("touchmove", cancelLongPress, { passive: true });
|
|
996
1026
|
row.addEventListener("touchend", cancelLongPress, { passive: true });
|
|
997
1027
|
row.addEventListener("touchcancel", cancelLongPress, { passive: true });
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
// to open a context menu used to wipe the multi-select set,
|
|
1008
|
-
// which was the user-reported "annoying" behavior.
|
|
1009
|
-
// - If it's NOT selected and we're NOT in multi-select →
|
|
1010
|
-
// single-select this row (replace prior selection).
|
|
1011
|
-
const body = row.parentElement;
|
|
1012
|
-
const inMulti = !!body?.classList.contains("multi-select-on");
|
|
1013
|
-
if (!row.classList.contains("selected")) {
|
|
1014
|
-
if (inMulti) {
|
|
1015
|
-
row.classList.add("selected");
|
|
1016
|
-
lastClickedRow = row;
|
|
1017
|
-
}
|
|
1018
|
-
else {
|
|
1019
|
-
clearSelection();
|
|
1020
|
-
row.classList.add("selected");
|
|
1021
|
-
lastClickedRow = row;
|
|
1022
|
-
focusMessage(msgAccountId, msg);
|
|
1023
|
-
}
|
|
1028
|
+
}
|
|
1029
|
+
onRowContextMenu(e) {
|
|
1030
|
+
e.preventDefault();
|
|
1031
|
+
const body = this.el.parentElement;
|
|
1032
|
+
const inMulti = !!body?.classList.contains("multi-select-on");
|
|
1033
|
+
if (!this.isSelected) {
|
|
1034
|
+
if (inMulti) {
|
|
1035
|
+
this.setSelected(true);
|
|
1036
|
+
lastClickedRow = this.el;
|
|
1024
1037
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
await updateFlags(msgAccountId, msg.uid, newFlags);
|
|
1051
|
-
msg.flags = newFlags;
|
|
1052
|
-
row.classList.toggle("flagged");
|
|
1053
|
-
flag.textContent = row.classList.contains("flagged") ? "\u2605" : "\u2606";
|
|
1054
|
-
}
|
|
1055
|
-
catch { /* ignore */ }
|
|
1056
|
-
},
|
|
1057
|
-
},
|
|
1058
|
-
{ label: "", action: () => { }, separator: true },
|
|
1059
|
-
{
|
|
1060
|
-
label: "Reply",
|
|
1061
|
-
action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "reply" } })),
|
|
1062
|
-
},
|
|
1063
|
-
{
|
|
1064
|
-
label: "Reply All",
|
|
1065
|
-
action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "replyAll" } })),
|
|
1066
|
-
},
|
|
1067
|
-
{
|
|
1068
|
-
label: "Forward",
|
|
1069
|
-
action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "forward" } })),
|
|
1070
|
-
},
|
|
1071
|
-
{ label: "", action: () => { }, separator: true },
|
|
1072
|
-
{
|
|
1073
|
-
label: "Move to folder…",
|
|
1074
|
-
action: async () => {
|
|
1075
|
-
// Move all currently-selected rows (or just this one if it's the only selection)
|
|
1076
|
-
const selectedRows = Array.from(document.querySelectorAll(".ml-row.selected"));
|
|
1077
|
-
const uids = selectedRows.length > 0
|
|
1078
|
-
? selectedRows.map((r) => Number(r.dataset.uid)).filter(u => !isNaN(u))
|
|
1079
|
-
: [msg.uid];
|
|
1080
|
-
const pick = await pickFolder(msgAccountId, { excludeFolderIds: [msg.folderId] });
|
|
1081
|
-
if (!pick)
|
|
1082
|
-
return;
|
|
1083
|
-
try {
|
|
1084
|
-
await apiMoveMessages(msgAccountId, uids, pick.folderId);
|
|
1085
|
-
// Remove from local state — reconciler handles server sync.
|
|
1086
|
-
state.removeMessages(uids.map(u => ({ accountId: msgAccountId, uid: u })));
|
|
1087
|
-
}
|
|
1088
|
-
catch (err) {
|
|
1089
|
-
alert(`Move failed: ${err.message}`);
|
|
1090
|
-
}
|
|
1091
|
-
},
|
|
1038
|
+
else {
|
|
1039
|
+
clearSelection();
|
|
1040
|
+
lastClickedRow = this.el;
|
|
1041
|
+
focusRow(this);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
const isSeen = this.msg.flags.includes("\\Seen");
|
|
1045
|
+
const isFlagged = this.msg.flags.includes("\\Flagged");
|
|
1046
|
+
const accountId = this.accountId;
|
|
1047
|
+
const msg = this.msg;
|
|
1048
|
+
const self = this;
|
|
1049
|
+
const items = [
|
|
1050
|
+
{
|
|
1051
|
+
label: isSeen ? "Mark unread" : "Mark read",
|
|
1052
|
+
action: async () => {
|
|
1053
|
+
const newFlags = isSeen
|
|
1054
|
+
? msg.flags.filter((f) => f !== "\\Seen")
|
|
1055
|
+
: [...msg.flags, "\\Seen"];
|
|
1056
|
+
try {
|
|
1057
|
+
await updateFlags(accountId, msg.uid, newFlags);
|
|
1058
|
+
msg.flags = newFlags;
|
|
1059
|
+
state.updateMessageFlags(accountId, msg.uid, newFlags);
|
|
1060
|
+
self.setUnreadClass(!newFlags.includes("\\Seen"));
|
|
1061
|
+
}
|
|
1062
|
+
catch { /* ignore */ }
|
|
1092
1063
|
},
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
label: isFlagged ? "Unflag" : "Flag",
|
|
1067
|
+
action: async () => {
|
|
1068
|
+
const newFlags = isFlagged
|
|
1069
|
+
? msg.flags.filter((f) => f !== "\\Flagged")
|
|
1070
|
+
: [...msg.flags, "\\Flagged"];
|
|
1071
|
+
try {
|
|
1072
|
+
await updateFlags(accountId, msg.uid, newFlags);
|
|
1073
|
+
msg.flags = newFlags;
|
|
1074
|
+
self.setFlaggedClass(newFlags.includes("\\Flagged"));
|
|
1075
|
+
}
|
|
1076
|
+
catch { /* ignore */ }
|
|
1096
1077
|
},
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1078
|
+
},
|
|
1079
|
+
{ label: "", action: () => { }, separator: true },
|
|
1080
|
+
{ label: "Reply", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "reply" } })) },
|
|
1081
|
+
{ label: "Reply All", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "replyAll" } })) },
|
|
1082
|
+
{ label: "Forward", action: () => document.dispatchEvent(new CustomEvent("mailx-compose", { detail: { mode: "forward" } })) },
|
|
1083
|
+
{ label: "", action: () => { }, separator: true },
|
|
1084
|
+
{
|
|
1085
|
+
label: "Move to folder…",
|
|
1086
|
+
action: async () => {
|
|
1087
|
+
const selectedRows = Array.from(document.querySelectorAll(".ml-row.selected"));
|
|
1088
|
+
const uids = selectedRows.length > 0
|
|
1089
|
+
? selectedRows.map((r) => Number(r.dataset.uid)).filter(u => !isNaN(u))
|
|
1090
|
+
: [msg.uid];
|
|
1091
|
+
const pick = await pickFolder(accountId, { excludeFolderIds: [msg.folderId] });
|
|
1092
|
+
if (!pick)
|
|
1093
|
+
return;
|
|
1094
|
+
try {
|
|
1095
|
+
await apiMoveMessages(accountId, uids, pick.folderId);
|
|
1096
|
+
removeMessagesAndReconcile(uids.map(u => ({ accountId, uid: u })));
|
|
1097
|
+
}
|
|
1098
|
+
catch (err) {
|
|
1099
|
+
alert(`Move failed: ${err.message}`);
|
|
1100
|
+
}
|
|
1101
1101
|
},
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
}
|
|
1102
|
+
},
|
|
1103
|
+
{ label: "Delete", action: () => document.dispatchEvent(new CustomEvent("mailx-delete")) },
|
|
1104
|
+
{ label: "", action: () => { }, separator: true },
|
|
1105
|
+
{ label: "⚠ Mark as spam", action: () => document.getElementById("btn-spam")?.click() },
|
|
1106
|
+
{ label: "", action: () => { }, separator: true },
|
|
1107
|
+
{
|
|
1108
|
+
label: "Copy Message-ID",
|
|
1109
|
+
action: async () => {
|
|
1110
|
+
if (!msg.messageId) {
|
|
1111
|
+
alert("No Message-ID on this row.");
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
try {
|
|
1115
|
+
await navigator.clipboard.writeText(msg.messageId);
|
|
1116
|
+
}
|
|
1117
|
+
catch { /* */ }
|
|
1117
1118
|
},
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1119
|
+
},
|
|
1120
|
+
];
|
|
1121
|
+
showContextMenu(e.clientX, e.clientY, items);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
function appendMessages(body, accountId, items) {
|
|
1125
|
+
// Thread grouping: when the list has the "threaded" class, collapse
|
|
1126
|
+
// messages sharing the same threadId to a single row showing the most
|
|
1127
|
+
// recent message, with a small pill indicating the thread size.
|
|
1128
|
+
const threaded = body.classList.contains("threaded");
|
|
1129
|
+
let rowsToRender = items;
|
|
1130
|
+
let threadSize = null;
|
|
1131
|
+
if (threaded) {
|
|
1132
|
+
const threadMap = new Map();
|
|
1133
|
+
threadSize = new Map();
|
|
1134
|
+
for (const msg of items) {
|
|
1135
|
+
const key = msg.threadId || `_msg_${msg.accountId || accountId}_${msg.uid}`;
|
|
1136
|
+
const existing = threadMap.get(key);
|
|
1137
|
+
if (!existing || (msg.date || 0) > (existing.date || 0)) {
|
|
1138
|
+
threadMap.set(key, msg);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
for (const msg of items) {
|
|
1142
|
+
const key = msg.threadId || `_msg_${msg.accountId || accountId}_${msg.uid}`;
|
|
1143
|
+
const head = threadMap.get(key);
|
|
1144
|
+
if (head)
|
|
1145
|
+
threadSize.set(head, (threadSize.get(head) || 0) + 1);
|
|
1146
|
+
}
|
|
1147
|
+
rowsToRender = Array.from(threadMap.values()).sort((a, b) => (b.date || 0) - (a.date || 0));
|
|
1148
|
+
}
|
|
1149
|
+
for (const msg of rowsToRender) {
|
|
1150
|
+
const msgAccountId = msg.accountId || accountId;
|
|
1151
|
+
const threadCount = threadSize ? (threadSize.get(msg) || 1) : 1;
|
|
1152
|
+
const isThreadHead = threadCount > 1 && !!msg.threadId;
|
|
1153
|
+
// showAccountTag: true when rendering the unified inbox (no
|
|
1154
|
+
// single accountId for the page; each row carries its own and
|
|
1155
|
+
// gets a one-letter account chip).
|
|
1156
|
+
const showAccountTag = !accountId && !!msgAccountId;
|
|
1157
|
+
const row = new MessageRow(msg, msgAccountId, isThreadHead, threadCount, showAccountTag);
|
|
1158
|
+
row.attach(body);
|
|
1122
1159
|
}
|
|
1123
1160
|
}
|
|
1124
1161
|
function formatDate(epochMs) {
|