mirror of
https://github.com/ChrispyBacon-dev/DockFlare.git
synced 2026-04-26 10:50:43 +00:00
v3.1.1 - RC - more testing needed - Details in Changelog.md
Some checks failed
Docker Image - Webmail / build_self_hosted (push) Waiting to run
Docker Image - Webmail / build_github_hosted_fallback (push) Blocked by required conditions
Docker Image - DockFlare / build_self_hosted (push) Has been cancelled
Docker Image - Mail Manager / build_self_hosted (push) Has been cancelled
Docker Image - DockFlare / build_github_hosted_fallback (push) Has been cancelled
Docker Image - Mail Manager / build_github_hosted_fallback (push) Has been cancelled
Some checks failed
Docker Image - Webmail / build_self_hosted (push) Waiting to run
Docker Image - Webmail / build_github_hosted_fallback (push) Blocked by required conditions
Docker Image - DockFlare / build_self_hosted (push) Has been cancelled
Docker Image - Mail Manager / build_self_hosted (push) Has been cancelled
Docker Image - DockFlare / build_github_hosted_fallback (push) Has been cancelled
Docker Image - Mail Manager / build_github_hosted_fallback (push) Has been cancelled
This commit is contained in:
parent
b3c0da2d75
commit
0fad7c592f
65 changed files with 5980 additions and 1619 deletions
22
CHANGELOG.md
22
CHANGELOG.md
|
|
@ -3,6 +3,28 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
|
||||
## [v3.1.1] - 2026-04-24
|
||||
|
||||
### Added
|
||||
- **Email Alias System:** A disposable email alias layer for the DockFlare Email Suite.
|
||||
- **Alias Creation:** Generate aliases with three styles (`word-word-num`, `word-num`, `uuid-short`) or define a custom local-part. Each alias is bound to a mailbox and routed transparently.
|
||||
- **Inbound Forwarding:** Aliases are enforced at the Cloudflare Worker layer via KV lookups mail to an unknown alias is rejected at the SMTP level before reaching the server. Quota enforcement follows the resolved mailbox.
|
||||
- **Outbound Reply Support:** When replying to an email received via an alias, the webmail compose view pre-selects the alias as the sender. A From dropdown appears whenever aliases are configured on the active mailbox.
|
||||
- **Alias Management UI:** Full alias management from the webmail Settings panel create, toggle active/inactive, set an optional expiry date, add a label and description, and delete. Usage count and last-use timestamp are tracked per alias.
|
||||
- **Alias Expiry:** An hourly background job deactivates expired aliases and removes their KV entries from Cloudflare automatically.
|
||||
- **Per-Alias Rate Limiting:** Maximum 20 alias creations per hour and 100 aliases per mailbox enforced server-side.
|
||||
- **Webmail Compose Enhancements:**
|
||||
- **Multi-Recipient To Field:** The compose dialog now supports multiple recipients in the To field. Addresses are entered as chips — press Enter, Tab, or comma to confirm. Paste a comma-separated list to add multiple at once.
|
||||
- **Cc and Bcc Fields:** Cc and Bcc fields are hidden by default and revealed via inline buttons.
|
||||
- **Sent Folder Recipient Display:** Message list items in the Sent folder now show recipient addresses instead of the sender's own address.
|
||||
- **Emoji Picker:** An emoji selector is available in the compose toolbar. Searchable, categorized, and lazy-loaded with no impact on initial bundle size.
|
||||
- **Inline Link Popover:** The insert-link toolbar button now opens an inline popover with a URL input and Apply/Cancel actions, replacing the browser-native prompt dialog.
|
||||
- **Mobile Support:** The webmail is now fully responsive for phones and small screens.
|
||||
- **Single-Panel Navigation:** On mobile, the three-panel desktop layout switches to a stacked single-panel view — Folders → Message List → Message Detail — with a back button to navigate up the stack.
|
||||
- **Bottom Navigation Bar:** A persistent bottom bar provides quick access to Folders, Compose, and Mail with a floating action button for compose.
|
||||
- **Full-Screen Compose:** The compose dialog opens as a full-screen overlay on mobile instead of the desktop floating popup.
|
||||
- **iOS Safe Area Support:** Bottom navigation respects the iOS home indicator safe area via `viewport-fit=cover`.
|
||||
|
||||
## [v3.1.0] - 2026-04-16
|
||||
|
||||
> **Cloudflare Context:** Cloudflare's Email Service entered public beta today — the same `send_email` Workers binding that powers DockFlare Mail's outbound relay is now generally available. Read the announcement: [Email for Agents](https://blog.cloudflare.com/email-for-agents/)
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ def _get_int_env(name, default, minimum=None):
|
|||
return default
|
||||
|
||||
# --- DockFlare Version ---
|
||||
APP_VERSION = "v3.1.0"
|
||||
APP_VERSION = "v3.1.1"
|
||||
# --- web: https://dockflare.app ---
|
||||
# --- github: https://github.com/ChrispyBacon-dev/DockFlare ---
|
||||
|
||||
|
|
|
|||
|
|
@ -337,6 +337,28 @@ def create_kv_namespace(title):
|
|||
json_data={'title': title})
|
||||
return res.get('result', {}).get('id')
|
||||
|
||||
|
||||
def get_or_create_kv_namespace(title):
|
||||
try:
|
||||
ns_id = create_kv_namespace(title)
|
||||
if ns_id:
|
||||
return ns_id
|
||||
except Exception:
|
||||
pass
|
||||
page = 1
|
||||
while True:
|
||||
res = cf_api_request('GET', f'/accounts/{config.CF_ACCOUNT_ID}/storage/kv/namespaces',
|
||||
params={'per_page': 100, 'page': page})
|
||||
results = res.get('result') or []
|
||||
for ns in results:
|
||||
if ns.get('title') == title:
|
||||
return ns.get('id')
|
||||
info = res.get('result_info', {})
|
||||
if page >= info.get('total_pages', 1):
|
||||
break
|
||||
page += 1
|
||||
return None
|
||||
|
||||
def update_kv_entry(namespace_id, key, value_dict):
|
||||
url = f"{config.CF_API_BASE_URL}/accounts/{config.CF_ACCOUNT_ID}/storage/kv/namespaces/{namespace_id}/values/{key}"
|
||||
headers = {"Authorization": f"Bearer {config.CF_API_TOKEN}", "Content-Type": "text/plain"}
|
||||
|
|
|
|||
|
|
@ -30,10 +30,11 @@ async function dispatchWebhook(env, payload) {
|
|||
}
|
||||
|
||||
export default {
|
||||
// ── Inbound email handler ──────────────────────────────────────────────────-.--...--
|
||||
async email(message, env, ctx) {
|
||||
try {
|
||||
let resolvedMailbox = null;
|
||||
const catchAllEnabled = env.CATCH_ALL_ENABLED === 'true';
|
||||
|
||||
if (catchAllEnabled) {
|
||||
const domain = (env.DOMAIN_NAME || '').toLowerCase();
|
||||
if (!message.to.toLowerCase().endsWith('@' + domain)) {
|
||||
|
|
@ -43,21 +44,28 @@ export default {
|
|||
} else {
|
||||
const allowedRecipients = JSON.parse(env.ALLOWED_RECIPIENTS || '[]');
|
||||
if (!allowedRecipients.includes(message.to)) {
|
||||
message.setReject("Recipient not allowed");
|
||||
return;
|
||||
let aliasRecord = null;
|
||||
try {
|
||||
aliasRecord = await env.QUOTA_KV.get('alias::' + message.to, 'json');
|
||||
} catch (_) {}
|
||||
|
||||
if (!aliasRecord) {
|
||||
message.setReject("Recipient not allowed");
|
||||
return;
|
||||
}
|
||||
resolvedMailbox = aliasRecord.mailbox;
|
||||
}
|
||||
}
|
||||
|
||||
// Check quota KV before accepting — reject at SMTP level so sender gets a bounce
|
||||
if (typeof env.QUOTA_KV !== 'undefined') {
|
||||
try {
|
||||
const state = await env.QUOTA_KV.get(message.to, "json");
|
||||
const quotaTarget = resolvedMailbox || message.to;
|
||||
const state = await env.QUOTA_KV.get(quotaTarget, "json");
|
||||
if (state?.blocked) {
|
||||
message.setReject("550 5.2.2 Mailbox full");
|
||||
return;
|
||||
}
|
||||
} catch (kvErr) {
|
||||
// KV unavailable — fall through, webhook safety net handles enforcement
|
||||
console.warn(`KV quota check failed for ${message.to}: ${kvErr.message}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -66,12 +74,13 @@ export default {
|
|||
const r2Key = `temp_cache/${messageId}.eml`;
|
||||
const receivedAt = new Date().toISOString();
|
||||
|
||||
// Upload to R2 first — email is now safely buffered regardless of what happens next
|
||||
const rawBytes = await new Response(message.raw).arrayBuffer();
|
||||
await env.EMAIL_BUCKET.put(r2Key, rawBytes, {
|
||||
customMetadata: {
|
||||
from: message.from,
|
||||
to: message.to,
|
||||
resolved_mailbox: resolvedMailbox || message.to,
|
||||
via_alias: resolvedMailbox ? "1" : "0",
|
||||
subject: message.headers.get("subject") || "",
|
||||
receivedAt: receivedAt
|
||||
}
|
||||
|
|
@ -81,6 +90,8 @@ export default {
|
|||
message_id: messageId,
|
||||
from: message.from,
|
||||
to: message.to,
|
||||
resolved_mailbox: resolvedMailbox || message.to,
|
||||
via_alias: !!resolvedMailbox,
|
||||
subject: message.headers.get("subject") || "",
|
||||
received_at: receivedAt,
|
||||
r2_key: r2Key,
|
||||
|
|
@ -92,31 +103,21 @@ export default {
|
|||
if (webhookResponse.ok) {
|
||||
const body = await webhookResponse.json().catch(() => ({}));
|
||||
if (body.reason === 'over_hard_quota') {
|
||||
// Mail Manager rejected the email (hard quota exceeded) and already cleaned
|
||||
// up R2 + the DB entry. Reject at SMTP level so sender gets an NDR bounce.
|
||||
message.setReject("550 5.2.2 Mailbox full");
|
||||
return;
|
||||
}
|
||||
// On success the mail-manager deletes the R2 file itself after processing.
|
||||
} else {
|
||||
// DockFlare returned an error — leave email in R2 for cron retry.
|
||||
// The email is safely buffered and will be delivered
|
||||
// automatically when DockFlare is healthy again.
|
||||
console.warn(`Webhook returned ${webhookResponse.status} for ${messageId} — buffered in R2 for retry`);
|
||||
}
|
||||
} catch (webhookErr) {
|
||||
// DockFlare is unreachable (offline, timeout, network error).
|
||||
// Email is already in R2. Cron will retry. Do NOT reject.
|
||||
console.warn(`Webhook unreachable for ${messageId} — buffered in R2 for retry: ${webhookErr.message}`);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
// Only reject if failed to store the email in R2 (truly unrecoverable). - reminder need some tests still
|
||||
message.setReject(`Worker error: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
// ── Cron trigger: retry buffered emails in R2 ─────────────────────────-..-.-.-.-────
|
||||
async scheduled(event, env, ctx) {
|
||||
console.log("Cron: scanning R2 temp_cache for buffered emails...");
|
||||
|
||||
|
|
@ -140,6 +141,8 @@ export default {
|
|||
message_id: messageId,
|
||||
from: meta.from || "",
|
||||
to: meta.to || "",
|
||||
resolved_mailbox: meta.resolved_mailbox || meta.to || "",
|
||||
via_alias: meta.via_alias === "1",
|
||||
subject: meta.subject || "",
|
||||
received_at: meta.receivedAt || new Date().toISOString(),
|
||||
r2_key: r2Key,
|
||||
|
|
@ -151,8 +154,6 @@ export default {
|
|||
if (response.ok) {
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (body.reason === 'over_hard_quota') {
|
||||
// Mailbox was full when cron retried — webhook already cleaned R2 + set KV block.
|
||||
// Count as processed (not a retry-able failure).
|
||||
console.warn(`Cron: buffered email ${messageId} rejected (over_hard_quota) — R2 cleaned by Mail Manager`);
|
||||
processed++;
|
||||
} else {
|
||||
|
|
@ -165,7 +166,6 @@ export default {
|
|||
failed++;
|
||||
}
|
||||
} catch (err) {
|
||||
// DockFlare still offline — will retry on next cron run
|
||||
console.warn(`Cron: DockFlare still unreachable for ${messageId}: ${err.message}`);
|
||||
failed++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,8 @@ export default {
|
|||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
const body = await request.json();
|
||||
const toHeader = Array.isArray(body.to) ? body.to.join(", ") : body.to;
|
||||
const rawTo = Array.isArray(body.to) ? body.to[0] : body.to;
|
||||
const addrMatch = typeof rawTo === 'string' ? rawTo.match(/<([^>]+)>/) : null;
|
||||
const toAddress = addrMatch ? addrMatch[1] : (typeof rawTo === 'string' ? rawTo.trim() : rawTo);
|
||||
const toList = Array.isArray(body.to) ? body.to : [body.to];
|
||||
const toHeader = toList.join(", ");
|
||||
|
||||
const attachments = Array.isArray(body.attachments) ? body.attachments.filter(a => a && a.data_b64) : [];
|
||||
const hasAttachments = attachments.length > 0;
|
||||
|
|
@ -73,9 +71,13 @@ export default {
|
|||
mimeMessage += `--${innerBoundary}--\r\n`;
|
||||
}
|
||||
|
||||
const message = new EmailMessage(body.from, toAddress, mimeMessage);
|
||||
try {
|
||||
await env.SEND_EMAIL.send(message);
|
||||
for (const recipient of toList) {
|
||||
const addrMatch = typeof recipient === 'string' ? recipient.match(/<([^>]+)>/) : null;
|
||||
const toAddress = addrMatch ? addrMatch[1] : (typeof recipient === 'string' ? recipient.trim() : recipient);
|
||||
const message = new EmailMessage(body.from, toAddress, mimeMessage);
|
||||
await env.SEND_EMAIL.send(message);
|
||||
}
|
||||
return new Response(JSON.stringify({ success: true, message_id: body.messageId }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
|
|
|
|||
|
|
@ -399,15 +399,15 @@ def _redeploy_inbound_worker(email_cfg, domain):
|
|||
|
||||
kv_ns_id = d.get('quota_kv_namespace_id')
|
||||
if not kv_ns_id:
|
||||
try:
|
||||
kv_ns_id = email_manager.create_kv_namespace(
|
||||
f"dockflare-quota-{domain.replace('.', '-')}"
|
||||
)
|
||||
kv_ns_id = email_manager.get_or_create_kv_namespace(
|
||||
f"dockflare-quota-{domain.replace('.', '-')}"
|
||||
)
|
||||
if kv_ns_id:
|
||||
email_cfg['domains'][domain]['quota_kv_namespace_id'] = kv_ns_id
|
||||
save_email_config(email_cfg)
|
||||
logging.info(f"Created quota KV namespace for existing domain {domain}: {kv_ns_id}")
|
||||
except Exception as e:
|
||||
logging.warning(f"Could not create quota KV namespace for {domain}: {e}")
|
||||
logging.info(f"Resolved quota KV namespace for {domain}: {kv_ns_id}")
|
||||
else:
|
||||
logging.warning(f"Could not resolve quota KV namespace for {domain}")
|
||||
|
||||
catch_all_address = d.get('catch_all_address', '')
|
||||
catch_all_enabled = 'true' if catch_all_address else 'false'
|
||||
|
|
@ -1190,6 +1190,78 @@ def quota_kv_sync():
|
|||
return jsonify({'status': 'ok', 'action': action})
|
||||
|
||||
|
||||
@email_bp.route('/internal/alias-kv-sync', methods=['POST'])
|
||||
def alias_kv_sync():
|
||||
if not _check_internal_request():
|
||||
return jsonify({'error': 'forbidden'}), 403
|
||||
data = request.get_json(force=True, silent=True) or {}
|
||||
domain = data.get('domain')
|
||||
alias_address = data.get('alias_address')
|
||||
action = data.get('action')
|
||||
mailbox_address = data.get('mailbox_address')
|
||||
if not domain or not alias_address or action not in ('put', 'delete'):
|
||||
return jsonify({'error': 'missing domain, alias_address, or valid action'}), 400
|
||||
if action == 'put' and not mailbox_address:
|
||||
return jsonify({'error': 'mailbox_address required for put'}), 400
|
||||
cfg = config.EMAIL_CONFIG
|
||||
if not cfg or domain not in cfg.get('domains', {}):
|
||||
return jsonify({'status': 'domain_not_found'}), 200
|
||||
kv_ns_id = cfg['domains'][domain].get('quota_kv_namespace_id')
|
||||
if not kv_ns_id:
|
||||
return jsonify({'status': 'no_kv'}), 200
|
||||
kv_key = f"alias::{alias_address}"
|
||||
try:
|
||||
if action == 'put':
|
||||
email_manager.update_kv_entry(kv_ns_id, kv_key, {"mailbox": mailbox_address})
|
||||
else:
|
||||
email_manager.delete_kv_entry(kv_ns_id, kv_key)
|
||||
except Exception as e:
|
||||
logging.warning(f"alias-kv-sync {action} failed for {alias_address}: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
return jsonify({'status': 'ok', 'action': action})
|
||||
|
||||
|
||||
@email_bp.route('/domain/<domain>/aliases', methods=['GET'])
|
||||
@login_required
|
||||
def domain_aliases(domain):
|
||||
import requests as req_lib
|
||||
token = _generate_jwt(current_user.get_id(), role='admin')
|
||||
if not token:
|
||||
return jsonify({'error': 'JWT configuration missing'}), 500
|
||||
try:
|
||||
resp = req_lib.get(
|
||||
f"{config.MAIL_MANAGER_INTERNAL_URL}/api/v1/system/aliases",
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
params={'domain': domain},
|
||||
timeout=10,
|
||||
)
|
||||
if resp.ok:
|
||||
return jsonify(resp.json())
|
||||
return jsonify({'error': 'mail-manager unreachable'}), 502
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@email_bp.route('/alias/<path:address>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def admin_delete_alias(address):
|
||||
import requests as req_lib
|
||||
token = _generate_jwt(current_user.get_id(), role='admin')
|
||||
if not token:
|
||||
return jsonify({'error': 'JWT configuration missing'}), 500
|
||||
try:
|
||||
resp = req_lib.delete(
|
||||
f"{config.MAIL_MANAGER_INTERNAL_URL}/api/v1/aliases/{address}",
|
||||
headers={'Authorization': f'Bearer {token}'},
|
||||
timeout=10,
|
||||
)
|
||||
if resp.ok:
|
||||
return jsonify({'status': 'deleted'})
|
||||
return jsonify({'error': resp.text[:200]}), resp.status_code
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
def _restart_mail_container():
|
||||
try:
|
||||
container = docker_client.containers.get('dockflare-mail-manager')
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ def gating_logic():
|
|||
return
|
||||
|
||||
if not current_user.is_authenticated:
|
||||
exempt_endpoints = ['static', 'web.ping', 'web.cloudflare_ping_route', 'setup.step_import_env', 'email.internal_mail_config', 'email.mailbox_login', 'email.quota_kv_sync']
|
||||
exempt_endpoints = ['static', 'web.ping', 'web.cloudflare_ping_route', 'setup.step_import_env', 'email.internal_mail_config', 'email.mailbox_login', 'email.quota_kv_sync', 'email.alias_kv_sync']
|
||||
oauth_endpoints = ['web.login_provider', 'web.auth_callback', 'web.login']
|
||||
if request.endpoint and not request.endpoint.startswith('auth.') and request.endpoint not in exempt_endpoints and request.endpoint not in oauth_endpoints:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import re
|
|||
import shutil
|
||||
import threading
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
import requests as http_requests
|
||||
from flask import Blueprint, request, jsonify, send_file
|
||||
|
|
@ -15,6 +15,7 @@ from app.config import config
|
|||
from app.core.database import get_db
|
||||
from app.api.middleware import jwt_required, admin_required
|
||||
from app.core.rate_limiter import limiter
|
||||
from app.core.alias_words import generate_alias, validate_alias_address
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
api_bp = Blueprint('api', __name__)
|
||||
|
|
@ -749,7 +750,7 @@ def _local_deliver(db, recipient_addr, from_address, to_field, data, subject, te
|
|||
log.info("Local delivery: msg_id=%s to=%s", local_msg_id, recipient_addr)
|
||||
|
||||
|
||||
def _dispatch_send(address, data):
|
||||
def _dispatch_send(address, data, effective_from=None, via_alias=None):
|
||||
allowed, reason = limiter.check_rate(address)
|
||||
if not allowed:
|
||||
return jsonify({"error": reason}), 429
|
||||
|
|
@ -760,6 +761,16 @@ def _dispatch_send(address, data):
|
|||
if not to_field:
|
||||
return jsonify({"error": "to is required"}), 400
|
||||
|
||||
cc_field = data.get('cc') or []
|
||||
if isinstance(cc_field, str):
|
||||
cc_field = [cc_field]
|
||||
data['cc'] = cc_field
|
||||
|
||||
bcc_field = data.get('bcc') or []
|
||||
if isinstance(bcc_field, str):
|
||||
bcc_field = [bcc_field]
|
||||
data['bcc'] = bcc_field
|
||||
|
||||
subject = data.get('subject', '')
|
||||
text = data.get('text') or data.get('text_body', '')
|
||||
html = data.get('html') or data.get('html_body', '')
|
||||
|
|
@ -773,6 +784,7 @@ def _dispatch_send(address, data):
|
|||
if total_attach > _MAX_ATTACH_BYTES:
|
||||
return jsonify({"error": "attachments exceed 10 MB limit"}), 413
|
||||
|
||||
from_address = effective_from or address
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
msg_id = f"<{uuid.uuid4()}@{address.split('@')[1]}>"
|
||||
|
||||
|
|
@ -807,7 +819,7 @@ def _dispatch_send(address, data):
|
|||
|
||||
if external_recipients:
|
||||
worker_payload = {
|
||||
"from": address,
|
||||
"from": from_address,
|
||||
"to": external_recipients,
|
||||
"cc": data.get('cc'),
|
||||
"bcc": data.get('bcc'),
|
||||
|
|
@ -840,8 +852,8 @@ def _dispatch_send(address, data):
|
|||
error_msg = 'Outbound worker not configured'
|
||||
|
||||
db.execute(
|
||||
"INSERT INTO send_log (message_id, from_address, to_addresses, subject, sent_at, status, error_message, worker_response) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(msg_id, address, json.dumps(to_field), subject, now, status, error_msg, worker_resp),
|
||||
"INSERT INTO send_log (message_id, from_address, to_addresses, subject, sent_at, status, error_message, worker_response, via_alias) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(msg_id, from_address, json.dumps(to_field), subject, now, status, error_msg, worker_resp, via_alias),
|
||||
)
|
||||
|
||||
if status == 'sent':
|
||||
|
|
@ -860,7 +872,7 @@ def _dispatch_send(address, data):
|
|||
reference_ids, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, 0, ?, ?, ?)
|
||||
""", (
|
||||
msg_id, address, sent_folder['id'], address,
|
||||
msg_id, address, sent_folder['id'], from_address,
|
||||
json.dumps(to_field), json.dumps(data.get('cc') or []),
|
||||
subject, text, html, now,
|
||||
data.get('in_reply_to') or data.get('inReplyTo', ''),
|
||||
|
|
@ -891,15 +903,12 @@ def send_email(address):
|
|||
if not _check_mailbox_access(address):
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
if request.content_type and 'multipart/form-data' in request.content_type:
|
||||
data = dict(request.form)
|
||||
# Flatten single-value lists produced by request.form
|
||||
data = {k: v[0] if isinstance(v, list) and len(v) == 1 else v for k, v in data.items()}
|
||||
data = {k: request.form.getlist(k) if len(request.form.getlist(k)) > 1 else request.form.get(k)
|
||||
for k in request.form.keys()}
|
||||
files = request.files.getlist('attachments')
|
||||
log.debug("send_email multipart: form_keys=%s file_count=%d", list(data.keys()), len(files))
|
||||
attachments = []
|
||||
for f in files:
|
||||
raw = f.read()
|
||||
log.debug("send_email attachment: filename=%s content_type=%s size=%d", f.filename, f.content_type, len(raw))
|
||||
attachments.append({
|
||||
'filename': f.filename,
|
||||
'content_type': f.content_type or 'application/octet-stream',
|
||||
|
|
@ -909,7 +918,24 @@ def send_email(address):
|
|||
data['attachments'] = attachments
|
||||
else:
|
||||
data = request.json or {}
|
||||
return _dispatch_send(address, data)
|
||||
|
||||
from_override = (data.get('from_address') or '').strip()
|
||||
effective_from = None
|
||||
via_alias = None
|
||||
|
||||
if from_override and from_override != address:
|
||||
db = get_db()
|
||||
now_iso = datetime.now(timezone.utc).isoformat()
|
||||
alias_row = db.execute(
|
||||
"SELECT address FROM aliases WHERE address=? AND mailbox_address=? AND is_active=1 AND (expires_at IS NULL OR expires_at > ?)",
|
||||
(from_override, address, now_iso)
|
||||
).fetchone()
|
||||
if not alias_row:
|
||||
return jsonify({"error": "unauthorized_sender"}), 403
|
||||
effective_from = from_override
|
||||
via_alias = from_override
|
||||
|
||||
return _dispatch_send(address, data, effective_from=effective_from, via_alias=via_alias)
|
||||
|
||||
|
||||
@api_bp.route('/mailboxes/<address>/drafts', methods=['POST'])
|
||||
|
|
@ -1233,3 +1259,264 @@ def list_auto_responders():
|
|||
db = get_db()
|
||||
rows = db.execute("SELECT mailbox_address, is_active FROM auto_responders").fetchall()
|
||||
return jsonify({"auto_responders": [dict(r) for r in rows]})
|
||||
|
||||
|
||||
_ALIAS_MAX_PER_MAILBOX = int(os.environ.get('ALIAS_MAX_PER_MAILBOX', 100))
|
||||
_ALIAS_RATE_LIMIT_PER_HOUR = 20
|
||||
|
||||
|
||||
def _sync_alias_kv(alias_address, mailbox_address, action):
|
||||
master_url = os.environ.get('DOCKFLARE_MASTER_URL', '').rstrip('/')
|
||||
if not master_url:
|
||||
return False
|
||||
payload = {"domain": alias_address.split('@')[1], "alias_address": alias_address, "action": action}
|
||||
if action == "put":
|
||||
payload["mailbox_address"] = mailbox_address
|
||||
try:
|
||||
resp = http_requests.post(
|
||||
f"{master_url}/email/internal/alias-kv-sync",
|
||||
json=payload,
|
||||
headers={"X-Bootstrap-Token": os.environ.get("INTERNAL_BOOTSTRAP_SECRET", "")},
|
||||
timeout=5,
|
||||
)
|
||||
if not resp.ok:
|
||||
log.error("alias-kv-sync %s failed for %s: HTTP %s %s", action, alias_address, resp.status_code, resp.text[:200])
|
||||
return resp.ok
|
||||
except Exception as e:
|
||||
log.error("alias-kv-sync %s failed for %s: %s", action, alias_address, e)
|
||||
return False
|
||||
|
||||
|
||||
def _alias_to_dict(row):
|
||||
return {
|
||||
"address": row["address"],
|
||||
"mailbox_address": row["mailbox_address"],
|
||||
"domain": row["domain"],
|
||||
"label": row["label"],
|
||||
"description": row["description"],
|
||||
"is_active": bool(row["is_active"]),
|
||||
"expires_at": row["expires_at"],
|
||||
"created_at": row["created_at"],
|
||||
"use_count": row["use_count"],
|
||||
"last_use_at": row["last_use_at"],
|
||||
}
|
||||
|
||||
|
||||
def _caller_mailboxes():
|
||||
user = request.user
|
||||
if user.get('role') == 'admin':
|
||||
return None
|
||||
return set(user.get('mailboxes', []))
|
||||
|
||||
|
||||
def _check_alias_ownership(alias_row):
|
||||
user = request.user
|
||||
if user.get('role') == 'admin':
|
||||
return True
|
||||
return alias_row['mailbox_address'] in user.get('mailboxes', [])
|
||||
|
||||
|
||||
@api_bp.route('/aliases', methods=['GET'])
|
||||
@jwt_required
|
||||
def list_aliases():
|
||||
db = get_db()
|
||||
caller_mailboxes = _caller_mailboxes()
|
||||
domain_filter = request.args.get('domain', '').strip()
|
||||
active_filter = request.args.get('active', '')
|
||||
label_filter = request.args.get('label', '').strip()
|
||||
mailbox_filter = request.args.get('mailbox', '').strip()
|
||||
|
||||
conditions = []
|
||||
params = []
|
||||
|
||||
if caller_mailboxes is not None:
|
||||
placeholders = ','.join('?' * len(caller_mailboxes))
|
||||
conditions.append(f"mailbox_address IN ({placeholders})")
|
||||
params.extend(list(caller_mailboxes))
|
||||
elif mailbox_filter:
|
||||
conditions.append("mailbox_address = ?")
|
||||
params.append(mailbox_filter)
|
||||
|
||||
if domain_filter:
|
||||
conditions.append("domain = ?")
|
||||
params.append(domain_filter)
|
||||
if active_filter != '':
|
||||
conditions.append("is_active = ?")
|
||||
params.append(1 if active_filter in ('1', 'true') else 0)
|
||||
if label_filter:
|
||||
conditions.append("label = ?")
|
||||
params.append(label_filter)
|
||||
|
||||
where = ("WHERE " + " AND ".join(conditions)) if conditions else ""
|
||||
rows = db.execute(f"SELECT * FROM aliases {where} ORDER BY created_at DESC", params).fetchall()
|
||||
aliases = [_alias_to_dict(r) for r in rows]
|
||||
|
||||
now_iso = datetime.now(timezone.utc).isoformat()
|
||||
total = len(aliases)
|
||||
active = sum(1 for a in aliases if a['is_active'] and (not a['expires_at'] or a['expires_at'] > now_iso))
|
||||
expired = sum(1 for a in aliases if a['expires_at'] and a['expires_at'] <= now_iso)
|
||||
disabled = sum(1 for a in aliases if not a['is_active'])
|
||||
|
||||
return jsonify({"aliases": aliases, "total": total, "active": active, "expired": expired, "disabled": disabled})
|
||||
|
||||
|
||||
@api_bp.route('/aliases/generate', methods=['POST'])
|
||||
@jwt_required
|
||||
def generate_alias_suggestion():
|
||||
data = request.json or {}
|
||||
mailbox_address = (data.get('mailbox_address') or '').strip()
|
||||
domain = (data.get('domain') or '').strip()
|
||||
style = data.get('style', 'word-word-num')
|
||||
|
||||
if not mailbox_address or not domain:
|
||||
return jsonify({"error": "mailbox_address and domain are required"}), 400
|
||||
if style not in ('word-word-num', 'word-num', 'uuid-short'):
|
||||
return jsonify({"error": "invalid style"}), 400
|
||||
|
||||
caller_mailboxes = _caller_mailboxes()
|
||||
if caller_mailboxes is not None and mailbox_address not in caller_mailboxes:
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
|
||||
db = get_db()
|
||||
if not db.execute("SELECT 1 FROM mailboxes WHERE address=? AND domain=?", (mailbox_address, domain)).fetchone():
|
||||
return jsonify({"error": "mailbox not found for domain"}), 404
|
||||
|
||||
suggestion = generate_alias(domain, style=style, db=db)
|
||||
return jsonify({"suggestion": suggestion, "available": True})
|
||||
|
||||
|
||||
@api_bp.route('/aliases', methods=['POST'])
|
||||
@jwt_required
|
||||
def create_alias():
|
||||
data = request.json or {}
|
||||
address = (data.get('address') or '').strip().lower()
|
||||
mailbox_address = (data.get('mailbox_address') or '').strip()
|
||||
label = (data.get('label') or '').strip() or None
|
||||
description = (data.get('description') or '').strip()[:200] or None
|
||||
expires_at = data.get('expires_at') or None
|
||||
|
||||
if not address or not mailbox_address:
|
||||
return jsonify({"error": "address and mailbox_address are required"}), 400
|
||||
|
||||
valid, err = validate_alias_address(address)
|
||||
if not valid:
|
||||
return jsonify({"error": err}), 400
|
||||
|
||||
alias_domain = address.split('@')[1]
|
||||
|
||||
caller_mailboxes = _caller_mailboxes()
|
||||
if caller_mailboxes is not None and mailbox_address not in caller_mailboxes:
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
|
||||
db = get_db()
|
||||
|
||||
if not db.execute("SELECT 1 FROM mailboxes WHERE address=? AND domain=?", (mailbox_address, alias_domain)).fetchone():
|
||||
return jsonify({"error": "mailbox not found or domain mismatch"}), 403
|
||||
|
||||
if db.execute("SELECT 1 FROM aliases WHERE address=?", (address,)).fetchone():
|
||||
return jsonify({"error": "alias already exists"}), 409
|
||||
if db.execute("SELECT 1 FROM mailboxes WHERE address=?", (address,)).fetchone():
|
||||
return jsonify({"error": "address already used as mailbox"}), 409
|
||||
|
||||
now_iso = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
if expires_at:
|
||||
if not isinstance(expires_at, str) or expires_at <= now_iso:
|
||||
return jsonify({"error": "expires_at must be a future ISO-8601 datetime"}), 400
|
||||
|
||||
cutoff = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
|
||||
rate_count = db.execute(
|
||||
"SELECT COUNT(*) FROM aliases WHERE mailbox_address=? AND created_at > ?",
|
||||
(mailbox_address, cutoff)
|
||||
).fetchone()[0]
|
||||
if rate_count >= _ALIAS_RATE_LIMIT_PER_HOUR:
|
||||
return jsonify({"error": "rate_limit", "message": "max 20 aliases per hour"}), 429
|
||||
|
||||
total_count = db.execute(
|
||||
"SELECT COUNT(*) FROM aliases WHERE mailbox_address=?", (mailbox_address,)
|
||||
).fetchone()[0]
|
||||
if total_count >= _ALIAS_MAX_PER_MAILBOX:
|
||||
return jsonify({"error": "alias_limit_reached", "message": f"max {_ALIAS_MAX_PER_MAILBOX} aliases per mailbox"}), 429
|
||||
|
||||
db.execute(
|
||||
"INSERT INTO aliases (address, mailbox_address, domain, label, description, is_active, expires_at, created_at, use_count) VALUES (?, ?, ?, ?, ?, 1, ?, ?, 0)",
|
||||
(address, mailbox_address, alias_domain, label, description, expires_at, now_iso)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
threading.Thread(target=_sync_alias_kv, args=(address, mailbox_address, "put"), daemon=True).start()
|
||||
|
||||
row = db.execute("SELECT * FROM aliases WHERE address=?", (address,)).fetchone()
|
||||
return jsonify(_alias_to_dict(row)), 201
|
||||
|
||||
|
||||
@api_bp.route('/aliases/<path:address>', methods=['GET'])
|
||||
@jwt_required
|
||||
def get_alias(address):
|
||||
db = get_db()
|
||||
row = db.execute("SELECT * FROM aliases WHERE address=?", (address,)).fetchone()
|
||||
if not row:
|
||||
return jsonify({"error": "not found"}), 404
|
||||
if not _check_alias_ownership(row):
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
return jsonify(_alias_to_dict(row))
|
||||
|
||||
|
||||
@api_bp.route('/aliases/<path:address>', methods=['PATCH'])
|
||||
@jwt_required
|
||||
def update_alias(address):
|
||||
db = get_db()
|
||||
row = db.execute("SELECT * FROM aliases WHERE address=?", (address,)).fetchone()
|
||||
if not row:
|
||||
return jsonify({"error": "not found"}), 404
|
||||
if not _check_alias_ownership(row):
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
|
||||
data = request.json or {}
|
||||
updates = {}
|
||||
kv_action = None
|
||||
|
||||
if 'is_active' in data:
|
||||
new_active = 1 if data['is_active'] else 0
|
||||
updates['is_active'] = new_active
|
||||
kv_action = "put" if new_active else "delete"
|
||||
|
||||
if 'label' in data:
|
||||
updates['label'] = (data['label'] or '').strip() or None
|
||||
if 'description' in data:
|
||||
updates['description'] = (data['description'] or '').strip()[:200] or None
|
||||
if 'expires_at' in data:
|
||||
updates['expires_at'] = data['expires_at'] or None
|
||||
|
||||
if not updates:
|
||||
return jsonify({"error": "no valid fields to update"}), 400
|
||||
|
||||
set_clause = ', '.join(f"{k}=?" for k in updates)
|
||||
db.execute(f"UPDATE aliases SET {set_clause} WHERE address=?", list(updates.values()) + [address])
|
||||
db.commit()
|
||||
|
||||
if kv_action:
|
||||
mailbox = row['mailbox_address']
|
||||
threading.Thread(target=_sync_alias_kv, args=(address, mailbox, kv_action), daemon=True).start()
|
||||
|
||||
row = db.execute("SELECT * FROM aliases WHERE address=?", (address,)).fetchone()
|
||||
return jsonify(_alias_to_dict(row))
|
||||
|
||||
|
||||
@api_bp.route('/aliases/<path:address>', methods=['DELETE'])
|
||||
@jwt_required
|
||||
def delete_alias(address):
|
||||
db = get_db()
|
||||
row = db.execute("SELECT * FROM aliases WHERE address=?", (address,)).fetchone()
|
||||
if not row:
|
||||
return jsonify({"error": "not found"}), 404
|
||||
if not _check_alias_ownership(row):
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
|
||||
mailbox = row['mailbox_address']
|
||||
db.execute("DELETE FROM aliases WHERE address=?", (address,))
|
||||
db.commit()
|
||||
|
||||
threading.Thread(target=_sync_alias_kv, args=(address, mailbox, "delete"), daemon=True).start()
|
||||
|
||||
return jsonify({"status": "deleted"})
|
||||
|
|
|
|||
|
|
@ -147,6 +147,32 @@ def wipe_all():
|
|||
config.IN_MAINTENANCE = False
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@system_bp.route('/aliases', methods=['GET'])
|
||||
@admin_required
|
||||
def list_aliases_system():
|
||||
domain = request.args.get('domain', '').strip()
|
||||
db_path = config.DB_PATH
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
if domain:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM aliases WHERE domain=? ORDER BY use_count DESC, created_at DESC",
|
||||
(domain,)
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM aliases ORDER BY use_count DESC, created_at DESC"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
aliases = [dict(r) for r in rows]
|
||||
for a in aliases:
|
||||
a['is_active'] = bool(a['is_active'])
|
||||
return jsonify({"domain": domain or None, "aliases": aliases, "total": len(aliases)})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
def _schedule_restart():
|
||||
def restart():
|
||||
import time
|
||||
|
|
|
|||
|
|
@ -211,26 +211,55 @@ def inbound():
|
|||
db = get_db()
|
||||
|
||||
to_address = ''
|
||||
alias_address = None
|
||||
|
||||
for addr in parsed['to_addresses']:
|
||||
cur = db.execute(
|
||||
"SELECT address FROM mailboxes WHERE address=?", (addr,)
|
||||
)
|
||||
if cur.fetchone():
|
||||
if db.execute("SELECT 1 FROM mailboxes WHERE address=?", (addr,)).fetchone():
|
||||
to_address = addr
|
||||
break
|
||||
|
||||
if not to_address:
|
||||
via_alias = data.get('via_alias', False)
|
||||
raw_resolved = data.get('resolved_mailbox', '')
|
||||
|
||||
if via_alias and raw_resolved:
|
||||
envelope_to = data.get('to', '')
|
||||
now_utc = datetime.now(timezone.utc).isoformat()
|
||||
alias_row = db.execute(
|
||||
"SELECT * FROM aliases WHERE address=? AND is_active=1 AND (expires_at IS NULL OR expires_at > ?)",
|
||||
(envelope_to, now_utc)
|
||||
).fetchone()
|
||||
if alias_row and alias_row['mailbox_address'] == raw_resolved:
|
||||
to_address = alias_row['mailbox_address']
|
||||
alias_address = envelope_to
|
||||
db.execute(
|
||||
"UPDATE aliases SET use_count=use_count+1, last_use_at=? WHERE address=?",
|
||||
(now_utc, alias_address)
|
||||
)
|
||||
else:
|
||||
now_utc = datetime.now(timezone.utc).isoformat()
|
||||
for addr in parsed['to_addresses']:
|
||||
alias_row = db.execute(
|
||||
"SELECT * FROM aliases WHERE address=? AND is_active=1 AND (expires_at IS NULL OR expires_at > ?)",
|
||||
(addr, now_utc)
|
||||
).fetchone()
|
||||
if alias_row:
|
||||
to_address = alias_row['mailbox_address']
|
||||
alias_address = addr
|
||||
db.execute(
|
||||
"UPDATE aliases SET use_count=use_count+1, last_use_at=? WHERE address=?",
|
||||
(now_utc, alias_address)
|
||||
)
|
||||
break
|
||||
|
||||
if not to_address and domain_cfg and domain_cfg['catch_all_mailbox']:
|
||||
catch_all = domain_cfg['catch_all_mailbox']
|
||||
if db.execute("SELECT 1 FROM mailboxes WHERE address=?", (catch_all,)).fetchone():
|
||||
to_address = catch_all
|
||||
|
||||
if not to_address:
|
||||
log.info("Inbound ignored: no matching mailbox for %s",
|
||||
parsed['to_addresses'])
|
||||
return jsonify({
|
||||
"status": "ignored",
|
||||
"reason": "unknown recipient",
|
||||
}), 200
|
||||
log.info("Inbound ignored: no matching mailbox for %s", parsed['to_addresses'])
|
||||
return jsonify({"status": "ignored", "reason": "unknown recipient"}), 200
|
||||
|
||||
cur = db.execute(
|
||||
"SELECT id FROM folders WHERE mailbox_address=? AND name='Inbox'",
|
||||
|
|
@ -253,8 +282,8 @@ def inbound():
|
|||
to_addresses, cc_addresses, bcc_addresses, subject, text_body,
|
||||
html_body, received_at, is_read, is_starred, is_draft,
|
||||
in_reply_to, reference_ids, size_bytes, has_attachments,
|
||||
headers_json, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0, ?, ?, ?, ?, ?, ?)
|
||||
headers_json, created_at, received_via_alias
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
parsed['message_id'], to_address, folder_id,
|
||||
parsed['from_address'], parsed['from_name'],
|
||||
|
|
@ -265,7 +294,7 @@ def inbound():
|
|||
parsed['received_at'], parsed['in_reply_to'],
|
||||
parsed['references'], actual_size,
|
||||
1 if parsed['attachments'] else 0,
|
||||
json.dumps(parsed['headers_json']), now,
|
||||
json.dumps(parsed['headers_json']), now, alias_address,
|
||||
))
|
||||
msg_id = cur.lastrowid
|
||||
|
||||
|
|
@ -399,6 +428,7 @@ def inbound():
|
|||
'subject': parsed['subject'],
|
||||
'from_name': parsed['from_name'] or parsed['from_address'],
|
||||
'mailbox': to_address,
|
||||
'via_alias': alias_address,
|
||||
})
|
||||
_check_and_send_auto_reply(db, to_address, parsed, domain_cfg)
|
||||
|
||||
|
|
|
|||
92
mail-manager/app/core/alias_expiry.py
Normal file
92
mail-manager/app/core/alias_expiry.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import logging
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import requests as http_requests
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _sync_alias_kv_delete(alias_address):
|
||||
master_url = os.environ.get('DOCKFLARE_MASTER_URL', '').rstrip('/')
|
||||
if not master_url:
|
||||
return
|
||||
try:
|
||||
http_requests.post(
|
||||
f"{master_url}/email/internal/alias-kv-sync",
|
||||
json={
|
||||
"domain": alias_address.split('@')[1],
|
||||
"alias_address": alias_address,
|
||||
"action": "delete",
|
||||
},
|
||||
headers={"X-Bootstrap-Token": os.environ.get("INTERNAL_BOOTSTRAP_SECRET", "")},
|
||||
timeout=5,
|
||||
)
|
||||
except Exception:
|
||||
log.error("alias-kv-sync delete failed during expiry for %s", alias_address)
|
||||
|
||||
|
||||
def _insert_expiry_message(db, mailbox_address, alias_address, now):
|
||||
inbox = db.execute(
|
||||
"SELECT id FROM folders WHERE mailbox_address=? AND name='Inbox'",
|
||||
(mailbox_address,)
|
||||
).fetchone()
|
||||
if not inbox:
|
||||
return
|
||||
alias_row = db.execute(
|
||||
"SELECT use_count FROM aliases WHERE address=?", (alias_address,)
|
||||
).fetchone()
|
||||
if not alias_row or alias_row['use_count'] == 0:
|
||||
return
|
||||
db.execute("""
|
||||
INSERT OR IGNORE INTO messages (
|
||||
message_id, mailbox_address, folder_id,
|
||||
from_address, from_name, to_addresses,
|
||||
cc_addresses, bcc_addresses, subject,
|
||||
text_body, html_body, received_at,
|
||||
is_read, is_starred, is_draft,
|
||||
in_reply_to, reference_ids, size_bytes,
|
||||
has_attachments, headers_json, created_at, is_system
|
||||
) VALUES (?, ?, ?, 'noreply@dockflare', 'DockFlare System', ?,
|
||||
'[]', '[]',
|
||||
?, ?, '', ?, 0, 0, 0,
|
||||
NULL, NULL, 0, 0, '{}', ?, 1)
|
||||
""", (
|
||||
f"alias-expired-{alias_address}-{now}",
|
||||
mailbox_address,
|
||||
inbox['id'],
|
||||
f'["{mailbox_address}"]',
|
||||
f"Alias expired: {alias_address}",
|
||||
f"The alias {alias_address} has reached its expiration date and is no longer active.\n\n"
|
||||
f"Emails sent to this address will no longer be delivered to your mailbox.",
|
||||
now,
|
||||
now,
|
||||
))
|
||||
|
||||
|
||||
def expire_aliases():
|
||||
from app.core.database import get_standalone_db
|
||||
|
||||
db = get_standalone_db()
|
||||
try:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
expired = db.execute(
|
||||
"SELECT address, mailbox_address FROM aliases "
|
||||
"WHERE is_active=1 AND expires_at IS NOT NULL AND expires_at < ?",
|
||||
(now,)
|
||||
).fetchall()
|
||||
|
||||
if not expired:
|
||||
return
|
||||
|
||||
for alias in expired:
|
||||
db.execute("UPDATE aliases SET is_active=0 WHERE address=?", (alias['address'],))
|
||||
_sync_alias_kv_delete(alias['address'])
|
||||
_insert_expiry_message(db, alias['mailbox_address'], alias['address'], now)
|
||||
log.info("Alias expired: %s -> %s", alias['address'], alias['mailbox_address'])
|
||||
|
||||
db.commit()
|
||||
except Exception:
|
||||
log.exception("alias expiry: unhandled error")
|
||||
finally:
|
||||
db.close()
|
||||
80
mail-manager/app/core/alias_words.py
Normal file
80
mail-manager/app/core/alias_words.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import re
|
||||
import random
|
||||
from uuid import uuid4
|
||||
|
||||
WORDS = [
|
||||
"able", "acorn", "acre", "aged", "agile", "airy", "alba", "alert", "aloe", "alto",
|
||||
"amber", "ample", "anchor", "arid", "ariel", "arrow", "aspen", "atlas", "azure", "baron",
|
||||
"basil", "beam", "bear", "beech", "birch", "blade", "bloom", "blue", "bold", "boon",
|
||||
"brake", "brave", "briar", "bridge", "bright", "brisk", "brook", "brush", "bulk", "buoy",
|
||||
"calm", "cape", "cedar", "chalk", "chase", "chime", "chip", "chord", "civic", "clamp",
|
||||
"clan", "claro", "clean", "clear", "cliff", "cloud", "clove", "coast", "comet", "coral",
|
||||
"crest", "crisp", "crop", "cross", "crust", "curl", "curve", "damp", "dawn", "deep",
|
||||
"delta", "dense", "depot", "dew", "dome", "door", "dove", "draft", "dune", "dust",
|
||||
"eagle", "echo", "edge", "elder", "elm", "ember", "epoch", "equal", "ever", "fable",
|
||||
"fair", "fawn", "fern", "field", "film", "fine", "fire", "firm", "fixed", "flag",
|
||||
"flame", "flare", "flat", "fleet", "flint", "float", "flood", "floor", "flow", "foam",
|
||||
"fond", "ford", "forge", "form", "forte", "frame", "frank", "free", "fresh", "frost",
|
||||
"full", "gale", "game", "gate", "gaze", "gear", "gild", "glade", "glare", "glow",
|
||||
"gold", "grade", "grain", "grand", "grant", "grape", "grass", "gravel", "gray", "green",
|
||||
"grove", "guide", "gulf", "hale", "halt", "haven", "hazel", "helm", "hill", "hive",
|
||||
"hold", "holly", "honor", "hope", "horn", "hull", "hunt", "inlet", "iron", "isle",
|
||||
"jade", "jasper", "jest", "jewel", "joint", "jump", "keen", "kelp", "kind", "king",
|
||||
"kite", "lake", "lance", "lark", "laser", "leaf", "lean", "ledge", "level", "light",
|
||||
"lime", "link", "lion", "loch", "lodge", "loft", "loom", "loop", "lore", "lotus",
|
||||
"lunar", "mace", "maple", "marsh", "mast", "meadow", "mesa", "mild", "mint", "mist",
|
||||
"moat", "mode", "moon", "moor", "moss", "mount", "mural", "naval", "noble", "noon",
|
||||
"north", "nova", "oaken", "ocean", "olive", "onyx", "open", "orbit", "otter", "ozone",
|
||||
"pact", "pale", "palm", "panel", "patch", "path", "peak", "pearl", "pine", "plain",
|
||||
"plank", "plant", "plate", "plaza", "plum", "polar", "pond", "pool", "port", "prime",
|
||||
"prism", "proof", "pure", "quail", "quartz", "quest", "quiet", "rail", "rain", "ramp",
|
||||
"range", "rapid", "raven", "realm", "reed", "reef", "reign", "ridge", "rift", "rigid",
|
||||
"ring", "river", "road", "robin", "rock", "rouge", "round", "route", "ruby", "rune",
|
||||
"rush", "sage", "sail", "salt", "sand", "satin", "scale", "scope", "scout", "seal",
|
||||
"shelf", "shim", "shore", "sigil", "silk", "silver", "skiff", "slate", "slick", "slope",
|
||||
"solar", "solid", "sonic", "span", "spark", "speed", "spire", "spray", "sprint", "spur",
|
||||
"stark", "star", "steam", "steel", "stern", "still", "stone", "storm", "strand", "stream",
|
||||
"stripe", "strong", "strut", "sunlit", "swift", "talon", "teal", "terra", "test", "tide",
|
||||
"timber", "titan", "tonal", "torch", "tower", "trace", "trail", "tram", "trend", "trove",
|
||||
"trunk", "tulip", "tuned", "ultra", "unity", "valid", "valor", "vault", "veil", "venom",
|
||||
"virid", "vivid", "vocal", "void", "ward", "wave", "weld", "wheat", "white", "wild",
|
||||
"willow", "wind", "wing", "winter", "wisp", "wren", "yard", "zeal", "zenith", "zinc",
|
||||
]
|
||||
|
||||
_ADDRESS_RE = re.compile(r'^[a-zA-Z0-9._+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$')
|
||||
|
||||
|
||||
def validate_alias_address(address):
|
||||
if not address or not isinstance(address, str):
|
||||
return False, "address required"
|
||||
if not _ADDRESS_RE.match(address):
|
||||
return False, "invalid address format"
|
||||
local, _, domain = address.partition('@')
|
||||
if not local or not domain:
|
||||
return False, "invalid address format"
|
||||
if '..' in address:
|
||||
return False, "invalid address format"
|
||||
return True, None
|
||||
|
||||
|
||||
def generate_alias(domain, style="word-word-num", db=None, max_attempts=20):
|
||||
for _ in range(max_attempts):
|
||||
if style == "word-word-num":
|
||||
candidate = f"{random.choice(WORDS)}.{random.choice(WORDS)}.{random.randint(100, 999)}@{domain}"
|
||||
elif style == "word-num":
|
||||
candidate = f"{random.choice(WORDS)}.{random.randint(1000, 9999)}@{domain}"
|
||||
else:
|
||||
candidate = f"{uuid4().hex[:10]}@{domain}"
|
||||
|
||||
if db is None or not _address_exists(db, candidate):
|
||||
return candidate
|
||||
|
||||
return f"{uuid4().hex[:10]}@{domain}"
|
||||
|
||||
|
||||
def _address_exists(db, address):
|
||||
if db.execute("SELECT 1 FROM aliases WHERE address=?", (address,)).fetchone():
|
||||
return True
|
||||
if db.execute("SELECT 1 FROM mailboxes WHERE address=?", (address,)).fetchone():
|
||||
return True
|
||||
return False
|
||||
|
|
@ -45,6 +45,7 @@ _SCHEMA = """
|
|||
has_attachments INTEGER,
|
||||
headers_json TEXT,
|
||||
created_at TEXT,
|
||||
received_via_alias TEXT,
|
||||
FOREIGN KEY(mailbox_address) REFERENCES mailboxes(address) ON DELETE CASCADE,
|
||||
FOREIGN KEY(folder_id) REFERENCES folders(id) ON DELETE CASCADE
|
||||
);
|
||||
|
|
@ -73,7 +74,8 @@ _SCHEMA = """
|
|||
sent_at TEXT,
|
||||
status TEXT,
|
||||
error_message TEXT,
|
||||
worker_response TEXT
|
||||
worker_response TEXT,
|
||||
via_alias TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS bounce_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
|
@ -94,11 +96,28 @@ _SCHEMA = """
|
|||
outbound_auth_secret TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS aliases (
|
||||
address TEXT PRIMARY KEY,
|
||||
mailbox_address TEXT NOT NULL,
|
||||
domain TEXT NOT NULL,
|
||||
label TEXT,
|
||||
description TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
expires_at TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
use_count INTEGER NOT NULL DEFAULT 0,
|
||||
last_use_at TEXT,
|
||||
FOREIGN KEY (mailbox_address) REFERENCES mailboxes(address) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_aliases_mailbox ON aliases(mailbox_address);
|
||||
CREATE INDEX IF NOT EXISTS idx_aliases_domain ON aliases(domain);
|
||||
CREATE INDEX IF NOT EXISTS idx_aliases_active ON aliases(is_active, expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_domain_configs_name ON domain_configs(domain_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_mailbox ON messages(mailbox_address);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_folder ON messages(folder_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_received ON messages(received_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_read ON messages(is_read);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_alias ON messages(received_via_alias) WHERE received_via_alias IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_attachments_message ON attachments(message_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_send_log_from ON send_log(from_address);
|
||||
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
||||
|
|
@ -119,7 +138,9 @@ _SCHEMA = """
|
|||
DROP TRIGGER IF EXISTS messages_ai;
|
||||
CREATE TRIGGER messages_ai AFTER INSERT ON messages BEGIN
|
||||
INSERT INTO messages_fts(rowid, subject, from_address, from_name, to_addresses, text_body)
|
||||
VALUES (new.id, new.subject, new.from_address, new.from_name, new.to_addresses, new.text_body);
|
||||
VALUES (new.id, new.subject, new.from_address, new.from_name,
|
||||
new.to_addresses || ' ' || COALESCE(new.received_via_alias, ''),
|
||||
new.text_body);
|
||||
END;
|
||||
DROP TRIGGER IF EXISTS messages_ad;
|
||||
CREATE TRIGGER messages_ad AFTER DELETE ON messages BEGIN
|
||||
|
|
@ -129,7 +150,9 @@ _SCHEMA = """
|
|||
CREATE TRIGGER messages_au AFTER UPDATE ON messages BEGIN
|
||||
DELETE FROM messages_fts WHERE rowid = old.id;
|
||||
INSERT INTO messages_fts(rowid, subject, from_address, from_name, to_addresses, text_body)
|
||||
VALUES (new.id, new.subject, new.from_address, new.from_name, new.to_addresses, new.text_body);
|
||||
VALUES (new.id, new.subject, new.from_address, new.from_name,
|
||||
new.to_addresses || ' ' || COALESCE(new.received_via_alias, ''),
|
||||
new.text_body);
|
||||
END;
|
||||
"""
|
||||
|
||||
|
|
@ -210,12 +233,36 @@ def _migrate(conn):
|
|||
"ALTER TABLE mailboxes ADD COLUMN last_quota_warning_at TEXT DEFAULT NULL",
|
||||
"ALTER TABLE domain_configs ADD COLUMN grace_buffer_bytes INTEGER DEFAULT NULL",
|
||||
"ALTER TABLE messages ADD COLUMN is_system INTEGER NOT NULL DEFAULT 0",
|
||||
"ALTER TABLE messages ADD COLUMN received_via_alias TEXT DEFAULT NULL",
|
||||
"ALTER TABLE send_log ADD COLUMN via_alias TEXT DEFAULT NULL",
|
||||
"CREATE INDEX IF NOT EXISTS idx_messages_alias ON messages(received_via_alias) WHERE received_via_alias IS NOT NULL",
|
||||
]:
|
||||
try:
|
||||
conn.execute(sql)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
conn.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS aliases (
|
||||
address TEXT PRIMARY KEY,
|
||||
mailbox_address TEXT NOT NULL,
|
||||
domain TEXT NOT NULL,
|
||||
label TEXT,
|
||||
description TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
expires_at TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
use_count INTEGER NOT NULL DEFAULT 0,
|
||||
last_use_at TEXT,
|
||||
FOREIGN KEY (mailbox_address) REFERENCES mailboxes(address) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_aliases_mailbox ON aliases(mailbox_address);
|
||||
CREATE INDEX IF NOT EXISTS idx_aliases_domain ON aliases(domain);
|
||||
CREATE INDEX IF NOT EXISTS idx_aliases_active ON aliases(is_active, expires_at);
|
||||
""")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
conn.executescript("""
|
||||
|
|
@ -250,8 +297,8 @@ def init_db():
|
|||
import logging
|
||||
os.makedirs(os.path.dirname(config.DB_PATH), exist_ok=True)
|
||||
conn = _connect()
|
||||
conn.executescript(_SCHEMA)
|
||||
_migrate(conn)
|
||||
conn.executescript(_SCHEMA)
|
||||
result = conn.execute("PRAGMA quick_check").fetchone()
|
||||
if result and result[0] != 'ok':
|
||||
logging.getLogger('mail-manager').critical("SQLite integrity check failed: %s", result[0])
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import time
|
|||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_INTERVAL = 7 * 24 * 3600
|
||||
_CLEANUP_INTERVAL = 7 * 24 * 3600
|
||||
_ALIAS_EXPIRY_INTERVAL = 3600
|
||||
|
||||
|
||||
def _run_cleanup():
|
||||
|
|
@ -47,7 +48,7 @@ def _run_cleanup():
|
|||
|
||||
def _scheduler_loop():
|
||||
while True:
|
||||
time.sleep(_INTERVAL)
|
||||
time.sleep(_CLEANUP_INTERVAL)
|
||||
log.info("Scheduler: running push subscription cleanup")
|
||||
try:
|
||||
_run_cleanup()
|
||||
|
|
@ -55,9 +56,23 @@ def _scheduler_loop():
|
|||
log.exception("Scheduler: unhandled error in cleanup run")
|
||||
|
||||
|
||||
def _alias_expiry_loop():
|
||||
time.sleep(300)
|
||||
while True:
|
||||
try:
|
||||
from app.core.alias_expiry import expire_aliases
|
||||
expire_aliases()
|
||||
except Exception:
|
||||
log.exception("Scheduler: unhandled error in alias expiry run")
|
||||
time.sleep(_ALIAS_EXPIRY_INTERVAL)
|
||||
|
||||
|
||||
def start_scheduler():
|
||||
if os.environ.get('DISABLE_SCHEDULER'):
|
||||
return
|
||||
t = threading.Thread(target=_scheduler_loop, daemon=True, name="push-cleanup-scheduler")
|
||||
t.start()
|
||||
log.info("Scheduler: push subscription cleanup scheduled every 7 days")
|
||||
a = threading.Thread(target=_alias_expiry_loop, daemon=True, name="alias-expiry-scheduler")
|
||||
a.start()
|
||||
log.info("Scheduler: alias expiry scheduled every hour")
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#0f172a" />
|
||||
<title>DockFlare Mail</title>
|
||||
<link rel="apple-touch-icon" href="/favicon/apple-touch-icon.png">
|
||||
|
|
|
|||
14
webmail/package-lock.json
generated
14
webmail/package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
|||
"name": "dockflare-webmail",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@tiptap/extension-color": "^2.27.2",
|
||||
"@tiptap/extension-font-family": "^2.27.2",
|
||||
"@tiptap/extension-highlight": "^2.27.2",
|
||||
|
|
@ -25,6 +26,7 @@
|
|||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"dompurify": "^3.4.0",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"lucide-vue-next": "^0.400.0",
|
||||
"pinia": "^2.2.0",
|
||||
"radix-vue": "^1.9.0",
|
||||
|
|
@ -1595,6 +1597,12 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emoji-mart/data": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz",
|
||||
"integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
|
|
@ -4285,6 +4293,12 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emoji-mart": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz",
|
||||
"integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@tiptap/extension-color": "^2.27.2",
|
||||
"@tiptap/extension-font-family": "^2.27.2",
|
||||
"@tiptap/extension-highlight": "^2.27.2",
|
||||
|
|
@ -25,6 +26,7 @@
|
|||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"dompurify": "^3.4.0",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"lucide-vue-next": "^0.400.0",
|
||||
"pinia": "^2.2.0",
|
||||
"radix-vue": "^1.9.0",
|
||||
|
|
|
|||
|
|
@ -9,5 +9,17 @@ export const authApi = {
|
|||
});
|
||||
return response.json();
|
||||
},
|
||||
changePassword: async (currentPassword, newPassword) => {
|
||||
const token = localStorage.getItem('jwt_token');
|
||||
const response = await fetch('/email/auth/change-password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }),
|
||||
});
|
||||
return response.json();
|
||||
},
|
||||
};
|
||||
//# sourceMappingURL=auth.js.map
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"auth.js","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,UAAU,CAAA;AAEhC,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC;IAE1C,iBAAiB,EAAE,KAAK,EAAE,KAAa,EAAE,QAAgB,EAAE,EAAE;QAC3D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,mBAAmB,EAAE;YAChD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;SAC1C,CAAC,CAAA;QACF,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAA;IACxB,CAAC;CACF,CAAA"}
|
||||
{"version":3,"file":"auth.js","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,UAAU,CAAA;AAEhC,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC;IAE1C,iBAAiB,EAAE,KAAK,EAAE,KAAa,EAAE,QAAgB,EAAE,EAAE;QAC3D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,mBAAmB,EAAE;YAChD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;SAC1C,CAAC,CAAA;QACF,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAA;IACxB,CAAC;IAED,cAAc,EAAE,KAAK,EAAE,eAAuB,EAAE,WAAmB,EAAE,EAAE;QACrE,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;QAC/C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,6BAA6B,EAAE;YAC1D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,eAAe,EAAE,UAAU,KAAK,EAAE;aACnC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,CAAC;SACvF,CAAC,CAAA;QACF,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAA;IACxB,CAAC;CACF,CAAA"}
|
||||
|
|
@ -12,7 +12,6 @@ apiClient.interceptors.request.use(config => {
|
|||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
// Let the browser set Content-Type (with boundary) for FormData
|
||||
if (config.data instanceof FormData) {
|
||||
delete config.headers['Content-Type'];
|
||||
}
|
||||
|
|
@ -21,6 +20,10 @@ apiClient.interceptors.request.use(config => {
|
|||
apiClient.interceptors.response.use(response => response, error => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('jwt_token');
|
||||
import('../stores/auth').then(({ useAuthStore }) => {
|
||||
const authStore = useAuthStore();
|
||||
authStore.token = '';
|
||||
});
|
||||
router.push('/login');
|
||||
}
|
||||
return Promise.reject(error);
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"client.js","sourceRoot":"","sources":["client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,MAAM,MAAM,WAAW,CAAA;AAE9B,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC;IAC7B,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,KAAK;IACd,OAAO,EAAE;QACP,cAAc,EAAE,kBAAkB;KACnC;CACF,CAAC,CAAA;AAEF,SAAS,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;IAC1C,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IAC/C,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,CAAC,OAAO,CAAC,aAAa,GAAG,UAAU,KAAK,EAAE,CAAA;IAClD,CAAC;IACD,gEAAgE;IAChE,IAAI,MAAM,CAAC,IAAI,YAAY,QAAQ,EAAE,CAAC;QACpC,OAAO,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;IACvC,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC,CAAC,CAAA;AAEF,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,CACjC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EACpB,KAAK,CAAC,EAAE;IACN,IAAI,KAAK,CAAC,QAAQ,EAAE,MAAM,KAAK,GAAG,EAAE,CAAC;QACnC,YAAY,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;QACpC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACvB,CAAC;IACD,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC9B,CAAC,CACF,CAAA;AAED,eAAe,SAAS,CAAA"}
|
||||
{"version":3,"file":"client.js","sourceRoot":"","sources":["client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,MAAM,MAAM,WAAW,CAAA;AAE9B,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC;IAC7B,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,KAAK;IACd,OAAO,EAAE;QACP,cAAc,EAAE,kBAAkB;KACnC;CACF,CAAC,CAAA;AAEF,SAAS,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;IAC1C,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IAC/C,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,CAAC,OAAO,CAAC,aAAa,GAAG,UAAU,KAAK,EAAE,CAAA;IAClD,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,YAAY,QAAQ,EAAE,CAAC;QACpC,OAAO,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;IACvC,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC,CAAC,CAAA;AAEF,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,CACjC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EACpB,KAAK,CAAC,EAAE;IACN,IAAI,KAAK,CAAC,QAAQ,EAAE,MAAM,KAAK,GAAG,EAAE,CAAC;QACnC,YAAY,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;QACpC,MAAM,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE;YACjD,MAAM,SAAS,GAAG,YAAY,EAAE,CAAA;YAChC,SAAS,CAAC,KAAK,GAAG,EAAE,CAAA;QACtB,CAAC,CAAC,CAAA;QACF,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACvB,CAAC;IACD,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC9B,CAAC,CACF,CAAA;AAED,eAAe,SAAS,CAAA"}
|
||||
|
|
@ -15,9 +15,20 @@ export const mailApi = {
|
|||
sendMessage: (address, data) => apiClient.post(`/mailboxes/${address}/send`, data),
|
||||
searchMessages: (address, params) => apiClient.get(`/mailboxes/${address}/search`, { params }),
|
||||
getMailboxStatus: () => apiClient.get('/mailboxes/status'),
|
||||
getMailboxPreferences: (address) => apiClient.get(`/mailboxes/${address}/preferences`),
|
||||
updateMailboxPreferences: (address, data) => apiClient.patch(`/mailboxes/${address}/preferences`, data),
|
||||
getAttachmentUrl: (id) => `/api/v1/attachments/${id}/download`,
|
||||
downloadAttachment: (id) => apiClient.get(`/attachments/${id}/download`, { responseType: 'blob' }).then(r => r.data),
|
||||
createDraft: (address, data) => apiClient.post(`/mailboxes/${address}/drafts`, data),
|
||||
updateDraft: (address, id, data) => apiClient.put(`/mailboxes/${address}/drafts/${id}`, data),
|
||||
getAutoResponder: (address) => apiClient.get(`/mailboxes/${address}/auto-responder`),
|
||||
setAutoResponder: (address, data) => apiClient.post(`/mailboxes/${address}/auto-responder`, data),
|
||||
deleteAutoResponder: (address) => apiClient.delete(`/mailboxes/${address}/auto-responder`),
|
||||
getAliases: (mailbox) => apiClient.get('/aliases', { params: { mailbox, active: 1 } }),
|
||||
getAllAliases: (mailbox) => apiClient.get('/aliases', { params: { mailbox } }),
|
||||
createAlias: (data) => apiClient.post('/aliases', data),
|
||||
updateAlias: (address, data) => apiClient.patch(`/aliases/${encodeURIComponent(address)}`, data),
|
||||
deleteAlias: (address) => apiClient.delete(`/aliases/${encodeURIComponent(address)}`),
|
||||
generateAlias: (mailbox, domain, style) => apiClient.post('/aliases/generate', { mailbox_address: mailbox, domain, style: style || 'word-word-num' }),
|
||||
};
|
||||
//# sourceMappingURL=mail.js.map
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"mail.js","sourceRoot":"","sources":["mail.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"mail.js","sourceRoot":"","sources":["mail.ts"],"names":[],"mappings":"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"}
|
||||
|
|
@ -37,4 +37,16 @@ export const mailApi = {
|
|||
apiClient.post(`/mailboxes/${address}/auto-responder`, data),
|
||||
deleteAutoResponder: (address: string) =>
|
||||
apiClient.delete(`/mailboxes/${address}/auto-responder`),
|
||||
getAliases: (mailbox: string) =>
|
||||
apiClient.get('/aliases', { params: { mailbox, active: 1 } }),
|
||||
getAllAliases: (mailbox: string) =>
|
||||
apiClient.get('/aliases', { params: { mailbox } }),
|
||||
createAlias: (data: Record<string, any>) =>
|
||||
apiClient.post('/aliases', data),
|
||||
updateAlias: (address: string, data: Record<string, any>) =>
|
||||
apiClient.patch(`/aliases/${encodeURIComponent(address)}`, data),
|
||||
deleteAlias: (address: string) =>
|
||||
apiClient.delete(`/aliases/${encodeURIComponent(address)}`),
|
||||
generateAlias: (mailbox: string, domain: string, style?: string) =>
|
||||
apiClient.post('/aliases/generate', { mailbox_address: mailbox, domain, style: style || 'word-word-num' }),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,15 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer utilities {
|
||||
.pb-safe {
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
.h-\[100dvh\] {
|
||||
height: 100dvh;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick, onUnmounted } from 'vue'
|
||||
import { ref, watch, nextTick, onUnmounted, onMounted, type Ref, computed } from 'vue'
|
||||
import { useBreakpoint } from '../../composables/useBreakpoint'
|
||||
import {
|
||||
Paperclip, X, Bold as BoldIcon, Italic as ItalicIcon, Link2, List as ListIcon, ListOrdered, Minus,
|
||||
Underline as UnderlineIcon, AlignLeft as AlignLeftIcon, AlignCenter as AlignCenterIcon, AlignRight as AlignRightIcon, AlignJustify as AlignJustifyIcon,
|
||||
Quote as QuoteIcon, RemoveFormatting, Baseline, Trash2, Strikethrough as StrikethroughIcon, Type, BookmarkCheck, Maximize2, Minimize2
|
||||
Quote as QuoteIcon, RemoveFormatting, Baseline, Trash2, Strikethrough as StrikethroughIcon, Type, BookmarkCheck, Maximize2, Minimize2, Smile
|
||||
} from 'lucide-vue-next'
|
||||
import { useEditor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
|
|
@ -22,11 +23,25 @@ import { useMailStore } from '../../stores/mail'
|
|||
import Button from '../ui/Button.vue'
|
||||
import Input from '../ui/Input.vue'
|
||||
|
||||
defineProps({ panelMode: { type: Boolean, default: false } })
|
||||
const props = defineProps({ panelMode: { type: Boolean, default: false } })
|
||||
|
||||
const store = useMailStore()
|
||||
const { isMobile } = useBreakpoint()
|
||||
const effectivePanelMode = computed(() => props.panelMode || isMobile.value)
|
||||
|
||||
const to = ref('')
|
||||
const _EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
|
||||
const toTags = ref<string[]>([])
|
||||
const toInput = ref('')
|
||||
const ccTags = ref<string[]>([])
|
||||
const ccInput = ref('')
|
||||
const bccTags = ref<string[]>([])
|
||||
const bccInput = ref('')
|
||||
const showCc = ref(false)
|
||||
const showBcc = ref(false)
|
||||
|
||||
const fromAddress = ref('')
|
||||
const aliases = ref<string[]>([])
|
||||
const subject = ref('')
|
||||
const attachments = ref<File[]>([])
|
||||
const sending = ref(false)
|
||||
|
|
@ -65,8 +80,24 @@ const editor = useEditor({
|
|||
},
|
||||
})
|
||||
|
||||
const loadAliases = async () => {
|
||||
if (!store.currentMailbox) return
|
||||
try {
|
||||
const res = await mailApi.getAliases(store.currentMailbox)
|
||||
aliases.value = (res.data.aliases || []).map((a: any) => a.address)
|
||||
} catch { aliases.value = [] }
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
to.value = ''
|
||||
toTags.value = []
|
||||
toInput.value = ''
|
||||
ccTags.value = []
|
||||
ccInput.value = ''
|
||||
bccTags.value = []
|
||||
bccInput.value = ''
|
||||
showCc.value = false
|
||||
showBcc.value = false
|
||||
fromAddress.value = store.currentMailbox || ''
|
||||
subject.value = ''
|
||||
attachments.value = []
|
||||
error.value = ''
|
||||
|
|
@ -78,17 +109,67 @@ const reset = () => {
|
|||
store.composeDefaults = null
|
||||
}
|
||||
|
||||
const addTag = (tags: Ref<string[]>, input: Ref<string>) => {
|
||||
const val = input.value.trim().replace(/[,;]+$/, '')
|
||||
if (val && _EMAIL_RE.test(val) && !tags.value.includes(val)) {
|
||||
tags.value.push(val)
|
||||
}
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
const makeTagHandlers = (tags: Ref<string[]>, input: Ref<string>) => ({
|
||||
onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter' || e.key === ',' || e.key === 'Tab') {
|
||||
e.preventDefault()
|
||||
addTag(tags, input)
|
||||
} else if (e.key === 'Backspace' && !input.value && tags.value.length) {
|
||||
tags.value.pop()
|
||||
}
|
||||
},
|
||||
onBlur() { addTag(tags, input) },
|
||||
onPaste(e: ClipboardEvent) {
|
||||
e.preventDefault()
|
||||
const text = e.clipboardData?.getData('text') || ''
|
||||
for (const addr of text.split(/[,;\s]+/)) {
|
||||
const trimmed = addr.trim()
|
||||
if (trimmed && _EMAIL_RE.test(trimmed) && !tags.value.includes(trimmed)) {
|
||||
tags.value.push(trimmed)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const toHandlers = makeTagHandlers(toTags, toInput)
|
||||
const ccHandlers = makeTagHandlers(ccTags, ccInput)
|
||||
const bccHandlers = makeTagHandlers(bccTags, bccInput)
|
||||
|
||||
onMounted(loadAliases)
|
||||
|
||||
watch(() => store.isComposeOpen, async (open) => {
|
||||
if (open && store.composeDefaults) {
|
||||
to.value = store.composeDefaults.to || ''
|
||||
subject.value = store.composeDefaults.subject || ''
|
||||
quotedHtml.value = store.composeDefaults.quotedHtml || ''
|
||||
if (store.composeDefaults.draftId) {
|
||||
draftId.value = store.composeDefaults.draftId
|
||||
if (open) {
|
||||
await loadAliases()
|
||||
if (store.composeDefaults) {
|
||||
const rawTo = store.composeDefaults.to || ''
|
||||
if (rawTo) {
|
||||
for (const addr of rawTo.split(',').map((s: string) => s.trim()).filter(Boolean)) {
|
||||
if (_EMAIL_RE.test(addr) && !toTags.value.includes(addr)) toTags.value.push(addr)
|
||||
}
|
||||
}
|
||||
subject.value = store.composeDefaults.subject || ''
|
||||
quotedHtml.value = store.composeDefaults.quotedHtml || ''
|
||||
if (store.composeDefaults.draftId) {
|
||||
draftId.value = store.composeDefaults.draftId
|
||||
}
|
||||
const requestedFrom = store.composeDefaults.from
|
||||
fromAddress.value = (requestedFrom && aliases.value.includes(requestedFrom))
|
||||
? requestedFrom
|
||||
: (store.currentMailbox || '')
|
||||
} else {
|
||||
fromAddress.value = store.currentMailbox || ''
|
||||
}
|
||||
minimized.value = false
|
||||
await nextTick()
|
||||
if (store.composeDefaults.body) {
|
||||
if (store.composeDefaults?.body) {
|
||||
editor.value?.commands.setContent(store.composeDefaults.body)
|
||||
} else {
|
||||
editor.value?.commands.clearContent()
|
||||
|
|
@ -98,24 +179,6 @@ watch(() => store.isComposeOpen, async (open) => {
|
|||
}
|
||||
}, { immediate: true })
|
||||
|
||||
watch(to, (val) => {
|
||||
if (store.composeDefaults !== null) {
|
||||
store.composeDefaults = { ...store.composeDefaults, to: val }
|
||||
}
|
||||
})
|
||||
|
||||
watch(subject, (val) => {
|
||||
if (store.composeDefaults !== null) {
|
||||
store.composeDefaults = { ...store.composeDefaults, subject: val }
|
||||
}
|
||||
})
|
||||
|
||||
watch(quotedHtml, (val) => {
|
||||
if (store.composeDefaults !== null) {
|
||||
store.composeDefaults = { ...store.composeDefaults, quotedHtml: val }
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => editor.value?.destroy())
|
||||
|
||||
const close = () => {
|
||||
|
|
@ -167,24 +230,44 @@ const formatBytes = (bytes: number) => {
|
|||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
const setLink = () => {
|
||||
const prev = editor.value?.getAttributes('link').href || ''
|
||||
const url = window.prompt('Enter URL', prev)
|
||||
if (url === null) return
|
||||
if (url === '') {
|
||||
const showLinkPopover = ref(false)
|
||||
const linkInput = ref('')
|
||||
|
||||
const openLinkPopover = () => {
|
||||
linkInput.value = editor.value?.getAttributes('link').href || ''
|
||||
showLinkPopover.value = true
|
||||
nextTick(() => {
|
||||
const el = document.getElementById('compose-link-input')
|
||||
el?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
const applyLink = () => {
|
||||
const url = linkInput.value.trim()
|
||||
if (!url) {
|
||||
editor.value?.chain().focus().unsetLink().run()
|
||||
} else {
|
||||
editor.value?.chain().focus().setLink({ href: url }).run()
|
||||
const href = url.startsWith('http') ? url : `https://${url}`
|
||||
editor.value?.chain().focus().setLink({ href }).run()
|
||||
}
|
||||
showLinkPopover.value = false
|
||||
}
|
||||
|
||||
const onLinkKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); applyLink() }
|
||||
if (e.key === 'Escape') { showLinkPopover.value = false }
|
||||
}
|
||||
|
||||
const saveDraft = async () => {
|
||||
if (!store.currentMailbox || !editor.value) return
|
||||
toHandlers.onBlur()
|
||||
savingDraft.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const payload = {
|
||||
to: to.value ? to.value.split(',').map((s: string) => s.trim()).filter(Boolean) : [],
|
||||
to: toTags.value,
|
||||
cc: ccTags.value,
|
||||
bcc: bccTags.value,
|
||||
subject: subject.value,
|
||||
html_body: editor.value.getHTML() + (quotedHtml.value || ''),
|
||||
text_body: editor.value.getText(),
|
||||
|
|
@ -209,6 +292,12 @@ const saveDraft = async () => {
|
|||
const send = async () => {
|
||||
if (!store.currentMailbox || !editor.value) return
|
||||
|
||||
toHandlers.onBlur()
|
||||
if (!toTags.value.length) {
|
||||
error.value = 'Please add at least one recipient.'
|
||||
return
|
||||
}
|
||||
|
||||
const totalSize = attachments.value.reduce((sum, f) => sum + f.size, 0)
|
||||
if (totalSize > MAX_ATTACHMENT_BYTES) {
|
||||
error.value = `Attachments exceed 10 MB limit (${formatBytes(totalSize)} total).`
|
||||
|
|
@ -221,10 +310,15 @@ const send = async () => {
|
|||
const html = editor.value.getHTML() + (quotedHtml.value || '')
|
||||
const text = editor.value.getText()
|
||||
const formData = new FormData()
|
||||
formData.append('to', to.value)
|
||||
for (const addr of toTags.value) formData.append('to', addr)
|
||||
for (const addr of ccTags.value) formData.append('cc', addr)
|
||||
for (const addr of bccTags.value) formData.append('bcc', addr)
|
||||
formData.append('subject', subject.value)
|
||||
formData.append('html', html)
|
||||
formData.append('text', text)
|
||||
if (fromAddress.value && fromAddress.value !== store.currentMailbox) {
|
||||
formData.append('from_address', fromAddress.value)
|
||||
}
|
||||
for (const file of attachments.value) {
|
||||
formData.append('attachments', file)
|
||||
}
|
||||
|
|
@ -266,18 +360,55 @@ const setHighlight = (e: Event) => {
|
|||
const target = e.target as HTMLInputElement
|
||||
editor.value?.chain().focus().setHighlight({ color: target.value }).run()
|
||||
}
|
||||
|
||||
const showEmojiPicker = ref(false)
|
||||
const emojiPickerContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
const openEmojiPicker = async () => {
|
||||
showEmojiPicker.value = !showEmojiPicker.value
|
||||
if (!showEmojiPicker.value) return
|
||||
await nextTick()
|
||||
if (!emojiPickerContainer.value) return
|
||||
emojiPickerContainer.value.innerHTML = ''
|
||||
const { Picker } = await import('emoji-mart')
|
||||
const data = (await import('@emoji-mart/data')).default
|
||||
new Picker({
|
||||
data,
|
||||
onEmojiSelect: (emoji: any) => {
|
||||
editor.value?.chain().focus().insertContent(emoji.native).run()
|
||||
showEmojiPicker.value = false
|
||||
},
|
||||
parent: emojiPickerContainer.value,
|
||||
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
|
||||
})
|
||||
}
|
||||
|
||||
const onEmojiClickOutside = (e: MouseEvent) => {
|
||||
if (!emojiPickerContainer.value) return
|
||||
const wrapper = emojiPickerContainer.value.closest('.emoji-picker-wrapper')
|
||||
if (wrapper && !wrapper.contains(e.target as Node)) {
|
||||
showEmojiPicker.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(showEmojiPicker, (val) => {
|
||||
if (val) document.addEventListener('mousedown', onEmojiClickOutside)
|
||||
else document.removeEventListener('mousedown', onEmojiClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => document.removeEventListener('mousedown', onEmojiClickOutside))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="store.isComposeOpen && (panelMode || !store.isComposeFullView)"
|
||||
:class="panelMode
|
||||
v-if="store.isComposeOpen && (effectivePanelMode || !store.isComposeFullView)"
|
||||
:class="effectivePanelMode
|
||||
? 'flex flex-col h-full w-full bg-background'
|
||||
: 'fixed bottom-0 right-6 z-50 flex flex-col rounded-t-xl shadow-2xl border border-border bg-background'"
|
||||
:style="!panelMode ? (minimized ? 'width:320px' : 'width:560px') : ''"
|
||||
:style="!effectivePanelMode ? (minimized ? 'width:320px' : 'width:620px') : ''"
|
||||
>
|
||||
<!-- Panel mode title bar -->
|
||||
<div v-if="panelMode" class="h-[52px] flex items-center gap-2 px-4 border-b border-border flex-shrink-0">
|
||||
<div v-if="effectivePanelMode" class="h-[52px] flex items-center gap-2 px-4 border-b border-border flex-shrink-0">
|
||||
<span class="flex-1 text-base font-semibold truncate">{{ subject || 'New Message' }}</span>
|
||||
<button type="button" class="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors" title="Pop out" @click="toggleFullView">
|
||||
<Minimize2 class="size-4" />
|
||||
|
|
@ -288,7 +419,7 @@ const setHighlight = (e: Event) => {
|
|||
</div>
|
||||
|
||||
<!-- Popup mode title bar -->
|
||||
<div v-else class="flex items-center gap-2 rounded-t-xl bg-primary px-4 py-2.5 cursor-pointer select-none" @click="toggleMinimize">
|
||||
<div v-else-if="!effectivePanelMode" class="flex items-center gap-2 rounded-t-xl bg-primary px-4 py-2.5 cursor-pointer select-none" @click="toggleMinimize">
|
||||
<span class="flex-1 text-sm font-semibold text-primary-foreground truncate">{{ subject || 'New Message' }}</span>
|
||||
<button type="button" class="rounded p-0.5 text-primary-foreground/70 hover:text-primary-foreground hover:bg-white/10 transition-colors" title="Full view" @click.stop="toggleFullView">
|
||||
<Maximize2 class="size-4" />
|
||||
|
|
@ -301,14 +432,94 @@ const setHighlight = (e: Event) => {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Body — always visible in panel mode, hidden when minimized in popup mode -->
|
||||
<!-- Body -->
|
||||
<div
|
||||
v-show="panelMode || !minimized"
|
||||
:class="panelMode ? 'flex flex-col flex-1 overflow-hidden' : 'flex flex-col flex-1 overflow-hidden max-h-[80vh]'"
|
||||
v-show="effectivePanelMode || !minimized"
|
||||
:class="effectivePanelMode ? 'flex flex-col flex-1 overflow-hidden' : 'flex flex-col flex-1 overflow-hidden max-h-[80vh]'"
|
||||
>
|
||||
<!-- Fields -->
|
||||
<div class="flex flex-col border-b border-border flex-shrink-0">
|
||||
<input v-model="to" placeholder="To" class="w-full border-b border-border px-4 py-2 text-sm bg-background text-foreground placeholder:text-muted-foreground focus:outline-none" />
|
||||
|
||||
<!-- To row -->
|
||||
<div class="flex items-start border-b border-border min-h-[36px]">
|
||||
<span class="px-4 py-2 text-sm text-muted-foreground shrink-0 leading-5">To</span>
|
||||
<div class="flex flex-wrap items-center gap-1 flex-1 py-1.5 pr-2 min-w-0">
|
||||
<span
|
||||
v-for="(tag, i) in toTags" :key="tag"
|
||||
class="flex items-center gap-1 bg-muted rounded-full px-2 py-0.5 text-xs text-foreground border border-border"
|
||||
>
|
||||
{{ tag }}
|
||||
<button type="button" @click="toTags.splice(i, 1)" class="hover:text-destructive leading-none"><X :size="10" /></button>
|
||||
</span>
|
||||
<input
|
||||
v-model="toInput"
|
||||
placeholder="Add recipient…"
|
||||
class="flex-1 min-w-[120px] bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none py-0.5"
|
||||
@keydown="toHandlers.onKeydown"
|
||||
@blur="toHandlers.onBlur"
|
||||
@paste="toHandlers.onPaste"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 px-3 py-2 shrink-0">
|
||||
<button v-if="!showCc" type="button" class="text-xs text-muted-foreground hover:text-foreground transition-colors" @click="showCc = true">Cc</button>
|
||||
<button v-if="!showBcc" type="button" class="text-xs text-muted-foreground hover:text-foreground transition-colors" @click="showBcc = true">Bcc</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cc row -->
|
||||
<div v-if="showCc" class="flex items-start border-b border-border min-h-[36px]">
|
||||
<span class="px-4 py-2 text-sm text-muted-foreground shrink-0 leading-5">Cc</span>
|
||||
<div class="flex flex-wrap items-center gap-1 flex-1 py-1.5 pr-2 min-w-0">
|
||||
<span
|
||||
v-for="(tag, i) in ccTags" :key="tag"
|
||||
class="flex items-center gap-1 bg-muted rounded-full px-2 py-0.5 text-xs text-foreground border border-border"
|
||||
>
|
||||
{{ tag }}
|
||||
<button type="button" @click="ccTags.splice(i, 1)" class="hover:text-destructive leading-none"><X :size="10" /></button>
|
||||
</span>
|
||||
<input
|
||||
v-model="ccInput"
|
||||
placeholder="Add Cc…"
|
||||
class="flex-1 min-w-[120px] bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none py-0.5"
|
||||
@keydown="ccHandlers.onKeydown"
|
||||
@blur="ccHandlers.onBlur"
|
||||
@paste="ccHandlers.onPaste"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bcc row -->
|
||||
<div v-if="showBcc" class="flex items-start border-b border-border min-h-[36px]">
|
||||
<span class="px-4 py-2 text-sm text-muted-foreground shrink-0 leading-5">Bcc</span>
|
||||
<div class="flex flex-wrap items-center gap-1 flex-1 py-1.5 pr-2 min-w-0">
|
||||
<span
|
||||
v-for="(tag, i) in bccTags" :key="tag"
|
||||
class="flex items-center gap-1 bg-muted rounded-full px-2 py-0.5 text-xs text-foreground border border-border"
|
||||
>
|
||||
{{ tag }}
|
||||
<button type="button" @click="bccTags.splice(i, 1)" class="hover:text-destructive leading-none"><X :size="10" /></button>
|
||||
</span>
|
||||
<input
|
||||
v-model="bccInput"
|
||||
placeholder="Add Bcc…"
|
||||
class="flex-1 min-w-[120px] bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none py-0.5"
|
||||
@keydown="bccHandlers.onKeydown"
|
||||
@blur="bccHandlers.onBlur"
|
||||
@paste="bccHandlers.onPaste"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- From row (aliases) -->
|
||||
<div v-if="aliases.length" class="flex items-center border-b border-border">
|
||||
<span class="px-4 py-2 text-sm text-muted-foreground shrink-0">From</span>
|
||||
<select v-model="fromAddress" class="flex-1 px-2 py-2 text-sm bg-background text-foreground focus:outline-none">
|
||||
<option :value="store.currentMailbox">{{ store.currentMailbox }}</option>
|
||||
<option v-for="alias in aliases" :key="alias" :value="alias">{{ alias }} (alias)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Subject -->
|
||||
<input v-model="subject" placeholder="Subject" class="w-full px-4 py-2 text-sm bg-background text-foreground placeholder:text-muted-foreground focus:outline-none" />
|
||||
</div>
|
||||
|
||||
|
|
@ -330,7 +541,7 @@ const setHighlight = (e: Event) => {
|
|||
|
||||
<div v-if="error" class="px-4 py-1 text-xs text-red-500 flex-shrink-0">{{ error }}</div>
|
||||
|
||||
<!-- Formatting Toolbar (Collapsible) -->
|
||||
<!-- Formatting Toolbar -->
|
||||
<div v-if="showFormatting" class="flex flex-wrap items-center gap-1 border-t border-border bg-muted/30 px-3 py-1.5 flex-shrink-0">
|
||||
<select @change="setFont" class="text-xs bg-transparent border-none focus:ring-0 text-foreground cursor-pointer mr-1 max-w-[100px]">
|
||||
<option value="">Default Font</option>
|
||||
|
|
@ -364,12 +575,18 @@ const setHighlight = (e: Event) => {
|
|||
<!-- Bottom Action Bar -->
|
||||
<div class="flex items-center justify-between gap-2 border-t border-border px-4 py-2.5 flex-shrink-0 bg-background">
|
||||
<div class="flex items-center gap-1">
|
||||
<Button as="button" type="button" size="sm" class="rounded-full px-5 font-semibold tracking-wide" @click.prevent="send" :disabled="sending || !to">
|
||||
<Button as="button" type="button" size="sm" class="rounded-full px-5 font-semibold tracking-wide" @click.prevent="send" :disabled="sending || (!toTags.length && !toInput)">
|
||||
{{ sending ? 'Sending…' : 'Send' }}
|
||||
</Button>
|
||||
<button type="button" class="ml-1 rounded p-1.5 transition-colors" :class="savedDraft ? 'text-green-500' : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'" :disabled="savingDraft" title="Save draft" @click="saveDraft">
|
||||
<BookmarkCheck class="size-4" />
|
||||
</button>
|
||||
<div class="relative emoji-picker-wrapper">
|
||||
<button type="button" class="rounded p-1.5 transition-colors" :class="showEmojiPicker ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'" @click="openEmojiPicker" title="Insert emoji">
|
||||
<Smile class="size-4" />
|
||||
</button>
|
||||
<div v-if="showEmojiPicker" class="absolute bottom-10 left-0 z-50 shadow-xl rounded-xl overflow-hidden" ref="emojiPickerContainer" />
|
||||
</div>
|
||||
<button type="button" class="ml-1 rounded p-1.5 hover:bg-accent transition-colors" :class="showFormatting ? 'bg-accent text-accent-foreground' : 'text-muted-foreground'" @click="showFormatting = !showFormatting" title="Formatting options">
|
||||
<Type class="size-4" />
|
||||
</button>
|
||||
|
|
@ -377,9 +594,26 @@ const setHighlight = (e: Event) => {
|
|||
<Paperclip class="size-4" />
|
||||
<input type="file" multiple class="hidden" @change="onFileChange" />
|
||||
</label>
|
||||
<button type="button" class="rounded p-1.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors" @click="setLink" title="Insert link">
|
||||
<Link2 class="size-4" />
|
||||
</button>
|
||||
<div class="relative">
|
||||
<button type="button" class="rounded p-1.5 transition-colors" :class="showLinkPopover ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'" @click="openLinkPopover" title="Insert link">
|
||||
<Link2 class="size-4" />
|
||||
</button>
|
||||
<div v-if="showLinkPopover" class="absolute bottom-10 left-0 z-50 w-72 rounded-lg border border-border bg-background shadow-xl p-3 flex flex-col gap-2">
|
||||
<label class="text-xs font-medium text-muted-foreground">Insert link</label>
|
||||
<input
|
||||
id="compose-link-input"
|
||||
v-model="linkInput"
|
||||
type="url"
|
||||
placeholder="https://example.com"
|
||||
class="w-full rounded-md border border-border bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
@keydown="onLinkKeydown"
|
||||
/>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button type="button" class="rounded-md px-3 py-1 text-xs text-muted-foreground hover:bg-accent transition-colors" @click="showLinkPopover = false">Cancel</button>
|
||||
<button type="button" class="rounded-md px-3 py-1 text-xs bg-primary text-primary-foreground hover:bg-primary/90 transition-colors" @click="applyLink">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="rounded p-1.5 text-muted-foreground hover:bg-accent hover:text-destructive transition-colors" :title="draftId ? 'Delete draft' : 'Discard'" @click="discardDraft">
|
||||
<Trash2 class="size-4" />
|
||||
|
|
@ -390,7 +624,6 @@ const setHighlight = (e: Event) => {
|
|||
</template>
|
||||
|
||||
<style>
|
||||
/* Tiptap editor inside compose */
|
||||
.compose-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -433,7 +666,6 @@ const setHighlight = (e: Event) => {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Pop-up animation */
|
||||
.compose-pop-enter-active { transition: transform 0.18s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.18s ease; }
|
||||
.compose-pop-leave-active { transition: transform 0.14s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.14s ease; }
|
||||
.compose-pop-enter-from { transform: translateY(24px) scale(0.95); opacity: 0; }
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
|
@ -48,8 +48,8 @@ const confirmNewFolder = async () => {
|
|||
store.folders = res.data;
|
||||
showNewFolder.value = false;
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to create folder', e);
|
||||
catch {
|
||||
store.showToast('Failed to create folder');
|
||||
}
|
||||
finally {
|
||||
creatingFolder.value = false;
|
||||
|
|
@ -69,8 +69,8 @@ const deleteFolder = async (f) => {
|
|||
store.currentFolder = store.folders[0]?.name || '';
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to delete folder', e);
|
||||
catch {
|
||||
store.showToast('Failed to delete folder');
|
||||
}
|
||||
};
|
||||
// ── Folder rename / colour edit ──────────────────────────────────────
|
||||
|
|
@ -100,8 +100,8 @@ const confirmEdit = async () => {
|
|||
}
|
||||
editingFolder.value = null;
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to rename folder', e);
|
||||
catch {
|
||||
store.showToast('Failed to rename folder');
|
||||
}
|
||||
};
|
||||
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
|
||||
|
|
@ -182,17 +182,19 @@ for (const [f] of __VLS_getVForSourceType((__VLS_ctx.store.folders))) {
|
|||
}, ...__VLS_functionalComponentArgsRest(__VLS_17));
|
||||
__VLS_19.slots.default;
|
||||
(f.name);
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
|
||||
...{ class: "ml-auto text-muted-foreground flex gap-1" },
|
||||
});
|
||||
if (f.unread_count) {
|
||||
if (f.total_count > 0) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
|
||||
...{ class: "font-bold" },
|
||||
...{ class: "ml-auto text-muted-foreground flex gap-1" },
|
||||
});
|
||||
(f.unread_count);
|
||||
if (f.unread_count) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
|
||||
...{ class: "font-bold" },
|
||||
});
|
||||
(f.unread_count);
|
||||
}
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
|
||||
(f.total_count);
|
||||
}
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
|
||||
(f.total_count || 0);
|
||||
var __VLS_19;
|
||||
var __VLS_15;
|
||||
var __VLS_3;
|
||||
|
|
@ -279,17 +281,19 @@ for (const [f] of __VLS_getVForSourceType((__VLS_ctx.store.folders))) {
|
|||
...{ class: "truncate" },
|
||||
});
|
||||
(f.name);
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
|
||||
...{ class: (__VLS_ctx.cn('ml-auto text-xs flex-shrink-0 flex gap-1', __VLS_ctx.store.currentFolder === f.name ? 'text-primary-foreground' : 'text-muted-foreground')) },
|
||||
});
|
||||
if (f.unread_count) {
|
||||
if (f.total_count > 0) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
|
||||
...{ class: "font-bold" },
|
||||
...{ class: (__VLS_ctx.cn('ml-auto text-xs flex-shrink-0 flex gap-1', __VLS_ctx.store.currentFolder === f.name ? 'text-primary-foreground' : 'text-muted-foreground')) },
|
||||
});
|
||||
(f.unread_count);
|
||||
if (f.unread_count) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
|
||||
...{ class: "font-bold" },
|
||||
});
|
||||
(f.unread_count);
|
||||
}
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
|
||||
(f.total_count);
|
||||
}
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
|
||||
(f.total_count || 0);
|
||||
if (!f.system_folder) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: (__VLS_ctx.cn('absolute right-1 flex gap-0.5 opacity-0 group-hover/row:opacity-100 transition-opacity rounded px-0.5', __VLS_ctx.store.currentFolder === f.name ? 'bg-primary' : 'bg-accent')) },
|
||||
|
|
@ -372,6 +376,16 @@ if (__VLS_ctx.showNewFolder && !__VLS_ctx.isCollapsed) {
|
|||
...{ style: (`background:${c}; border-color:${__VLS_ctx.newFolderColor === c ? '#000' : 'transparent'}`) },
|
||||
});
|
||||
}
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
|
||||
...{ onClick: (...[$event]) => {
|
||||
if (!(__VLS_ctx.showNewFolder && !__VLS_ctx.isCollapsed))
|
||||
return;
|
||||
__VLS_ctx.newFolderColor = '';
|
||||
} },
|
||||
...{ class: "h-5 w-5 rounded-full border-2 text-xs flex items-center justify-center text-muted-foreground hover:bg-accent" },
|
||||
...{ style: (`border-color:${!__VLS_ctx.newFolderColor ? '#888' : 'transparent'}`) },
|
||||
title: "No colour",
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "flex gap-1 justify-end" },
|
||||
});
|
||||
|
|
@ -531,6 +545,16 @@ if (!__VLS_ctx.isCollapsed) {
|
|||
/** @type {__VLS_StyleScopedClasses['border-2']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['transition-transform']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['hover:scale-110']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['h-5']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['w-5']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['rounded-full']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['border-2']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['hover:bg-accent']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['gap-1']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['justify-end']} */ ;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -3,10 +3,9 @@ import {
|
|||
SplitterGroup, SplitterPanel, SplitterResizeHandle,
|
||||
TooltipProvider, TooltipRoot, TooltipTrigger, TooltipContent, TooltipPortal,
|
||||
} from 'radix-vue'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { PenSquare, Sun, Moon, LogOut, Settings, Columns2, Maximize2 } from 'lucide-vue-next'
|
||||
import { defineAsyncComponent, ref, watch, computed } from 'vue'
|
||||
import { PenSquare, Sun, Moon, LogOut, Settings, Columns2, Maximize2, ChevronLeft, Menu } from 'lucide-vue-next'
|
||||
import { cn } from '../../lib/utils'
|
||||
import Separator from '../ui/Separator.vue'
|
||||
import MailboxSelector from './MailboxSelector.vue'
|
||||
import FolderNav from './FolderNav.vue'
|
||||
import MessageList from './MessageList.vue'
|
||||
|
|
@ -14,11 +13,13 @@ import MessageDisplay from './MessageDisplay.vue'
|
|||
import ComposeDialog from './ComposeDialog.vue'
|
||||
import { useMailStore } from '../../stores/mail'
|
||||
import { useAuth } from '../../composables/useAuth'
|
||||
import { useBreakpoint } from '../../composables/useBreakpoint'
|
||||
|
||||
const SettingsDialog = defineAsyncComponent(() => import('./SettingsDialog.vue'))
|
||||
|
||||
const store = useMailStore()
|
||||
const { logout } = useAuth()
|
||||
const { isMobile } = useBreakpoint()
|
||||
|
||||
const onCollapse = () => { store.isCollapsed = true }
|
||||
const onExpand = () => { store.isCollapsed = false }
|
||||
|
|
@ -27,11 +28,134 @@ const compose = () => {
|
|||
store.composeDefaults = null
|
||||
store.isComposeOpen = true
|
||||
}
|
||||
|
||||
// ── Mobile navigation stack ──────────────────────────────────────────
|
||||
type MobilePanel = 'folders' | 'list' | 'detail'
|
||||
const mobilePanel = ref<MobilePanel>('list')
|
||||
|
||||
watch(() => store.currentFolder, () => {
|
||||
if (isMobile.value) mobilePanel.value = 'list'
|
||||
})
|
||||
|
||||
watch(() => store.currentMessage, (msg) => {
|
||||
if (isMobile.value && msg) mobilePanel.value = 'detail'
|
||||
})
|
||||
|
||||
const goBack = () => {
|
||||
if (mobilePanel.value === 'detail') {
|
||||
store.currentMessage = null
|
||||
mobilePanel.value = 'list'
|
||||
} else if (mobilePanel.value === 'list') {
|
||||
mobilePanel.value = 'folders'
|
||||
}
|
||||
}
|
||||
|
||||
const mobileTitle = computed(() => {
|
||||
if (mobilePanel.value === 'folders') return store.currentMailbox || 'Folders'
|
||||
if (mobilePanel.value === 'list') return store.currentFolder || 'Inbox'
|
||||
return store.currentMessage?.subject || 'Message'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TooltipProvider :delay-duration="0">
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════
|
||||
MOBILE LAYOUT (< 768px)
|
||||
══════════════════════════════════════════════════════════ -->
|
||||
<div v-if="isMobile" class="flex flex-col h-[100dvh] w-screen bg-background overflow-hidden">
|
||||
|
||||
<!-- Top bar -->
|
||||
<div class="h-14 flex items-center gap-2 px-3 border-b border-border flex-shrink-0 bg-background">
|
||||
<button
|
||||
v-if="mobilePanel !== 'folders'"
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent transition-colors flex-shrink-0"
|
||||
@click="goBack"
|
||||
>
|
||||
<ChevronLeft class="size-5" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent transition-colors flex-shrink-0"
|
||||
@click="store.isSettingsOpen = true"
|
||||
>
|
||||
<Settings class="size-4" />
|
||||
</button>
|
||||
|
||||
<span class="flex-1 text-base font-semibold truncate">{{ mobileTitle }}</span>
|
||||
|
||||
<button
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent transition-colors flex-shrink-0"
|
||||
@click="store.toggleTheme()"
|
||||
>
|
||||
<Sun v-if="store.isDark" class="size-4" />
|
||||
<Moon v-else class="size-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent transition-colors flex-shrink-0"
|
||||
@click="logout"
|
||||
>
|
||||
<LogOut class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Panel content -->
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
|
||||
<!-- Folders panel -->
|
||||
<div v-if="mobilePanel === 'folders'" class="h-full flex flex-col overflow-y-auto">
|
||||
<div class="px-3 py-3 border-b border-border">
|
||||
<MailboxSelector :is-collapsed="false" />
|
||||
</div>
|
||||
<FolderNav :is-collapsed="false" />
|
||||
</div>
|
||||
|
||||
<!-- Message list panel -->
|
||||
<div v-else-if="mobilePanel === 'list'" class="h-full flex flex-col overflow-hidden">
|
||||
<MessageList />
|
||||
</div>
|
||||
|
||||
<!-- Message detail panel -->
|
||||
<div v-else-if="mobilePanel === 'detail'" class="h-full flex flex-col overflow-hidden">
|
||||
<MessageDisplay :message="store.currentMessage ?? undefined" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom nav bar -->
|
||||
<div class="h-16 flex items-center justify-around border-t border-border flex-shrink-0 bg-background pb-safe">
|
||||
<button
|
||||
class="flex flex-col items-center gap-0.5 px-4 py-2 rounded-lg transition-colors"
|
||||
:class="mobilePanel === 'folders' ? 'text-primary' : 'text-muted-foreground hover:text-foreground'"
|
||||
@click="mobilePanel = 'folders'"
|
||||
>
|
||||
<Menu class="size-5" />
|
||||
<span class="text-[10px] font-medium">Folders</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex items-center justify-center h-12 w-12 rounded-full bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-colors"
|
||||
@click="compose"
|
||||
>
|
||||
<PenSquare class="size-5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex flex-col items-center gap-0.5 px-4 py-2 rounded-lg transition-colors"
|
||||
:class="mobilePanel === 'list' ? 'text-primary' : 'text-muted-foreground hover:text-foreground'"
|
||||
@click="mobilePanel = 'list'"
|
||||
>
|
||||
<Columns2 class="size-5" />
|
||||
<span class="text-[10px] font-medium">Mail</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════
|
||||
DESKTOP LAYOUT (≥ 768px) — unchanged
|
||||
══════════════════════════════════════════════════════════ -->
|
||||
<SplitterGroup
|
||||
v-else
|
||||
id="mail-layout"
|
||||
direction="horizontal"
|
||||
class="h-screen w-screen items-stretch"
|
||||
|
|
@ -50,7 +174,6 @@ const compose = () => {
|
|||
@collapse="onCollapse"
|
||||
@expand="onExpand"
|
||||
>
|
||||
<!-- ── Single header row (same height as middle + right panel headers) ── -->
|
||||
<div
|
||||
:class="cn(
|
||||
'h-[52px] flex items-center gap-1 px-2 flex-shrink-0',
|
||||
|
|
@ -58,11 +181,9 @@ const compose = () => {
|
|||
)"
|
||||
>
|
||||
<template v-if="!store.isCollapsed">
|
||||
<!-- Mailbox selector fills available space -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<MailboxSelector :is-collapsed="false" />
|
||||
</div>
|
||||
<!-- Compose -->
|
||||
<TooltipRoot :delay-duration="0">
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
|
|
@ -74,7 +195,6 @@ const compose = () => {
|
|||
</button>
|
||||
</TooltipTrigger>
|
||||
</TooltipRoot>
|
||||
<!-- View Mode toggle -->
|
||||
<TooltipRoot :delay-duration="0">
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
|
|
@ -91,7 +211,6 @@ const compose = () => {
|
|||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</TooltipRoot>
|
||||
<!-- Theme toggle -->
|
||||
<TooltipRoot :delay-duration="0">
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
|
|
@ -108,7 +227,6 @@ const compose = () => {
|
|||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</TooltipRoot>
|
||||
<!-- Settings -->
|
||||
<TooltipRoot :delay-duration="0">
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
|
|
@ -124,7 +242,6 @@ const compose = () => {
|
|||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</TooltipRoot>
|
||||
<!-- Logout -->
|
||||
<TooltipRoot :delay-duration="0">
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
|
|
@ -142,7 +259,6 @@ const compose = () => {
|
|||
</TooltipRoot>
|
||||
</template>
|
||||
|
||||
<!-- Collapsed: stacked icon buttons -->
|
||||
<template v-else>
|
||||
<TooltipRoot :delay-duration="0">
|
||||
<TooltipTrigger as-child>
|
||||
|
|
@ -185,7 +301,6 @@ const compose = () => {
|
|||
<TooltipContent side="right" class="z-50 rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md">Logout</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</TooltipRoot>
|
||||
<!-- Collapsed mailbox icon below -->
|
||||
<MailboxSelector :is-collapsed="true" />
|
||||
</template>
|
||||
</div>
|
||||
|
|
@ -242,7 +357,15 @@ const compose = () => {
|
|||
</template>
|
||||
</SplitterGroup>
|
||||
|
||||
<ComposeDialog />
|
||||
<!-- Floating compose (desktop only) + settings dialog -->
|
||||
<template v-if="!isMobile">
|
||||
<ComposeDialog />
|
||||
</template>
|
||||
<Teleport v-else to="body">
|
||||
<div v-if="store.isComposeOpen" class="fixed inset-0 z-50 flex flex-col bg-background">
|
||||
<ComposeDialog :panel-mode="true" />
|
||||
</div>
|
||||
</Teleport>
|
||||
<SettingsDialog />
|
||||
</TooltipProvider>
|
||||
</template>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
|
@ -81,6 +81,15 @@ watch(emailIframe, (el) => {
|
|||
|
||||
onUnmounted(() => resizeObserver?.disconnect())
|
||||
|
||||
const parseAddrs = (raw: string | null | undefined) => {
|
||||
let addrs: string[] = []
|
||||
try { addrs = JSON.parse(raw || '[]') } catch { addrs = [] }
|
||||
return addrs.map((a: string) => { const m = a.match(/<([^>]+)>/); return m ? m[1] : a }).join(', ')
|
||||
}
|
||||
const toDisplay = computed(() => parseAddrs(props.message?.to_addresses))
|
||||
const ccDisplay = computed(() => parseAddrs(props.message?.cc_addresses))
|
||||
const bccDisplay = computed(() => parseAddrs(props.message?.bcc_addresses))
|
||||
|
||||
const displayTimestamp = computed(() => {
|
||||
const ts = props.message?.received_at || props.message?.sent_at
|
||||
return ts ? format(new Date(ts), 'PPpp') : ''
|
||||
|
|
@ -104,6 +113,7 @@ const replyTo = () => {
|
|||
if (!props.message) return
|
||||
store.composeDefaults = {
|
||||
to: props.message.from_address,
|
||||
from: props.message.received_via_alias || undefined,
|
||||
subject: props.message.subject?.startsWith('Re:')
|
||||
? props.message.subject
|
||||
: `Re: ${props.message.subject || ''}`,
|
||||
|
|
@ -126,6 +136,7 @@ const replyAll = () => {
|
|||
].filter((a: string) => a && a !== store.currentMailbox)
|
||||
store.composeDefaults = {
|
||||
to: allAddresses.join(', '),
|
||||
from: props.message.received_via_alias || undefined,
|
||||
subject: props.message.subject?.startsWith('Re:')
|
||||
? props.message.subject
|
||||
: `Re: ${props.message.subject || ''}`,
|
||||
|
|
@ -495,10 +506,16 @@ const sendInlineReply = async () => {
|
|||
<Avatar :initials="message.from_name?.[0] || message.from_address?.[0] || '?'" />
|
||||
<div class="grid gap-1">
|
||||
<div class="font-semibold">{{ message.from_name || message.from_address }}</div>
|
||||
<div class="line-clamp-1 text-xs">{{ message.subject }}</div>
|
||||
<div class="line-clamp-1 text-xs">
|
||||
<span class="font-medium">Reply-To:</span> {{ message.from_address }}
|
||||
<div v-if="toDisplay" class="line-clamp-1 text-xs">
|
||||
<span class="font-medium">To:</span> {{ toDisplay }}
|
||||
</div>
|
||||
<div v-if="ccDisplay" class="line-clamp-1 text-xs">
|
||||
<span class="font-medium">Cc:</span> {{ ccDisplay }}
|
||||
</div>
|
||||
<div v-if="bccDisplay" class="line-clamp-1 text-xs">
|
||||
<span class="font-medium">Bcc:</span> {{ bccDisplay }}
|
||||
</div>
|
||||
<div class="line-clamp-1 text-xs">{{ message.subject }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="displayTimestamp" class="ml-auto text-xs text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -65,6 +65,19 @@ watch(emailIframe, (el) => {
|
|||
resizeObserver.observe(el.parentElement);
|
||||
});
|
||||
onUnmounted(() => resizeObserver?.disconnect());
|
||||
const parseAddrs = (raw) => {
|
||||
let addrs = [];
|
||||
try {
|
||||
addrs = JSON.parse(raw || '[]');
|
||||
}
|
||||
catch {
|
||||
addrs = [];
|
||||
}
|
||||
return addrs.map((a) => { const m = a.match(/<([^>]+)>/); return m ? m[1] : a; }).join(', ');
|
||||
};
|
||||
const toDisplay = computed(() => parseAddrs(props.message?.to_addresses));
|
||||
const ccDisplay = computed(() => parseAddrs(props.message?.cc_addresses));
|
||||
const bccDisplay = computed(() => parseAddrs(props.message?.bcc_addresses));
|
||||
const displayTimestamp = computed(() => {
|
||||
const ts = props.message?.received_at || props.message?.sent_at;
|
||||
return ts ? format(new Date(ts), 'PPpp') : '';
|
||||
|
|
@ -85,6 +98,7 @@ const replyTo = () => {
|
|||
return;
|
||||
store.composeDefaults = {
|
||||
to: props.message.from_address,
|
||||
from: props.message.received_via_alias || undefined,
|
||||
subject: props.message.subject?.startsWith('Re:')
|
||||
? props.message.subject
|
||||
: `Re: ${props.message.subject || ''}`,
|
||||
|
|
@ -96,13 +110,28 @@ const replyTo = () => {
|
|||
const replyAll = () => {
|
||||
if (!props.message)
|
||||
return;
|
||||
let toList = [];
|
||||
let ccList = [];
|
||||
try {
|
||||
toList = JSON.parse(props.message.to_addresses || '[]');
|
||||
}
|
||||
catch {
|
||||
toList = [];
|
||||
}
|
||||
try {
|
||||
ccList = JSON.parse(props.message.cc_addresses || '[]');
|
||||
}
|
||||
catch {
|
||||
ccList = [];
|
||||
}
|
||||
const allAddresses = [
|
||||
props.message.from_address,
|
||||
...(JSON.parse(props.message.to_addresses || '[]')),
|
||||
...(JSON.parse(props.message.cc_addresses || '[]')),
|
||||
...toList,
|
||||
...ccList,
|
||||
].filter((a) => a && a !== store.currentMailbox);
|
||||
store.composeDefaults = {
|
||||
to: allAddresses.join(', '),
|
||||
from: props.message.received_via_alias || undefined,
|
||||
subject: props.message.subject?.startsWith('Re:')
|
||||
? props.message.subject
|
||||
: `Re: ${props.message.subject || ''}`,
|
||||
|
|
@ -134,9 +163,11 @@ const trash = async () => {
|
|||
await mailApi.deleteMessage(store.currentMailbox, props.message.id);
|
||||
store.messages = store.messages.filter((m) => m.id !== props.message.id);
|
||||
store.currentMessage = null;
|
||||
const fRes = await mailApi.getFolders(store.currentMailbox);
|
||||
store.folders = fRes.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to trash message', e);
|
||||
catch {
|
||||
store.showToast('Failed to move message to trash');
|
||||
}
|
||||
};
|
||||
const markUnread = async () => {
|
||||
|
|
@ -147,10 +178,28 @@ const markUnread = async () => {
|
|||
const idx = store.messages.findIndex((m) => m.id === props.message.id);
|
||||
if (idx !== -1)
|
||||
store.messages[idx] = { ...store.messages[idx], is_read: 0 };
|
||||
store.currentMessage = null;
|
||||
store.currentMessage = { ...store.currentMessage, is_read: 0 };
|
||||
const fRes = await mailApi.getFolders(store.currentMailbox);
|
||||
store.folders = fRes.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to mark unread', e);
|
||||
catch {
|
||||
store.showToast('Failed to mark as unread');
|
||||
}
|
||||
};
|
||||
const markRead = async () => {
|
||||
if (!props.message || !store.currentMailbox)
|
||||
return;
|
||||
try {
|
||||
await mailApi.updateMessage(store.currentMailbox, props.message.id, { is_read: true });
|
||||
const idx = store.messages.findIndex((m) => m.id === props.message.id);
|
||||
if (idx !== -1)
|
||||
store.messages[idx] = { ...store.messages[idx], is_read: 1 };
|
||||
store.currentMessage = { ...store.currentMessage, is_read: 1 };
|
||||
const fRes = await mailApi.getFolders(store.currentMailbox);
|
||||
store.folders = fRes.data;
|
||||
}
|
||||
catch {
|
||||
store.showToast('Failed to mark as read');
|
||||
}
|
||||
};
|
||||
const toggleStar = async () => {
|
||||
|
|
@ -165,8 +214,8 @@ const toggleStar = async () => {
|
|||
if (store.currentMessage)
|
||||
store.currentMessage = { ...store.currentMessage, is_starred: newVal };
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to toggle star', e);
|
||||
catch {
|
||||
store.showToast('Failed to update star');
|
||||
}
|
||||
};
|
||||
const moveToFolder = async (targetFolder) => {
|
||||
|
|
@ -179,17 +228,26 @@ const moveToFolder = async (targetFolder) => {
|
|||
});
|
||||
store.messages = store.messages.filter((m) => m.id !== props.message.id);
|
||||
store.currentMessage = null;
|
||||
const fRes = await mailApi.getFolders(store.currentMailbox);
|
||||
store.folders = fRes.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to move message', e);
|
||||
catch {
|
||||
store.showToast('Failed to move message');
|
||||
}
|
||||
};
|
||||
const escapeHtml = (str) => str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
const printMessage = () => {
|
||||
if (!props.message)
|
||||
return;
|
||||
const from = props.message.from_name ? `${props.message.from_name} <${props.message.from_address}>` : props.message.from_address;
|
||||
const toRaw = JSON.parse(props.message.to_addresses || '[]');
|
||||
const to = Array.isArray(toRaw) ? toRaw.join(', ') : toRaw;
|
||||
let toRaw = [];
|
||||
try {
|
||||
toRaw = JSON.parse(props.message.to_addresses || '[]');
|
||||
}
|
||||
catch {
|
||||
toRaw = [];
|
||||
}
|
||||
const to = Array.isArray(toRaw) ? toRaw.join(', ') : String(toRaw);
|
||||
const date = displayTimestamp.value;
|
||||
const subject = props.message.subject || '(No Subject)';
|
||||
let content = '';
|
||||
|
|
@ -225,9 +283,9 @@ const printMessage = () => {
|
|||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1 class="subject">${subject}</h1>
|
||||
<div class="meta"><div class="label">From:</div><div class="val">${from.replace(/</g, '<').replace(/>/g, '>')}</div></div>
|
||||
<div class="meta"><div class="label">To:</div><div class="val">${to.replace(/</g, '<').replace(/>/g, '>')}</div></div>
|
||||
<h1 class="subject">${escapeHtml(subject)}</h1>
|
||||
<div class="meta"><div class="label">From:</div><div class="val">${escapeHtml(from)}</div></div>
|
||||
<div class="meta"><div class="label">To:</div><div class="val">${escapeHtml(to)}</div></div>
|
||||
<div class="meta"><div class="label">Date:</div><div class="val">${date}</div></div>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
|
@ -248,6 +306,8 @@ const printMessage = () => {
|
|||
const sendInlineReply = async () => {
|
||||
if (!props.message || !store.currentMailbox || !replyText.value.trim())
|
||||
return;
|
||||
if (!props.message.from_address)
|
||||
return;
|
||||
sendingReply.value = true;
|
||||
try {
|
||||
await mailApi.sendMessage(store.currentMailbox, {
|
||||
|
|
@ -261,8 +321,8 @@ const sendInlineReply = async () => {
|
|||
});
|
||||
replyText.value = '';
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to send reply', e);
|
||||
catch {
|
||||
store.showToast('Failed to send reply');
|
||||
}
|
||||
finally {
|
||||
sendingReply.value = false;
|
||||
|
|
@ -882,137 +942,169 @@ const __VLS_223 = __VLS_222({
|
|||
...{ class: "z-50 min-w-[160px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_222));
|
||||
__VLS_224.slots.default;
|
||||
const __VLS_225 = {}.DropdownMenuItem;
|
||||
if (props.message?.is_read) {
|
||||
const __VLS_225 = {}.DropdownMenuItem;
|
||||
/** @type {[typeof __VLS_components.DropdownMenuItem, typeof __VLS_components.DropdownMenuItem, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_226 = __VLS_asFunctionalComponent(__VLS_225, new __VLS_225({
|
||||
...{ 'onClick': {} },
|
||||
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
|
||||
}));
|
||||
const __VLS_227 = __VLS_226({
|
||||
...{ 'onClick': {} },
|
||||
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_226));
|
||||
let __VLS_229;
|
||||
let __VLS_230;
|
||||
let __VLS_231;
|
||||
const __VLS_232 = {
|
||||
onClick: (__VLS_ctx.markUnread)
|
||||
};
|
||||
__VLS_228.slots.default;
|
||||
const __VLS_233 = {}.MailOpen;
|
||||
/** @type {[typeof __VLS_components.MailOpen, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_234 = __VLS_asFunctionalComponent(__VLS_233, new __VLS_233({
|
||||
...{ class: "mr-2 size-4" },
|
||||
}));
|
||||
const __VLS_235 = __VLS_234({
|
||||
...{ class: "mr-2 size-4" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_234));
|
||||
var __VLS_228;
|
||||
}
|
||||
else {
|
||||
const __VLS_237 = {}.DropdownMenuItem;
|
||||
/** @type {[typeof __VLS_components.DropdownMenuItem, typeof __VLS_components.DropdownMenuItem, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_238 = __VLS_asFunctionalComponent(__VLS_237, new __VLS_237({
|
||||
...{ 'onClick': {} },
|
||||
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
|
||||
}));
|
||||
const __VLS_239 = __VLS_238({
|
||||
...{ 'onClick': {} },
|
||||
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_238));
|
||||
let __VLS_241;
|
||||
let __VLS_242;
|
||||
let __VLS_243;
|
||||
const __VLS_244 = {
|
||||
onClick: (__VLS_ctx.markRead)
|
||||
};
|
||||
__VLS_240.slots.default;
|
||||
const __VLS_245 = {}.MailOpen;
|
||||
/** @type {[typeof __VLS_components.MailOpen, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_246 = __VLS_asFunctionalComponent(__VLS_245, new __VLS_245({
|
||||
...{ class: "mr-2 size-4" },
|
||||
}));
|
||||
const __VLS_247 = __VLS_246({
|
||||
...{ class: "mr-2 size-4" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_246));
|
||||
var __VLS_240;
|
||||
}
|
||||
const __VLS_249 = {}.DropdownMenuItem;
|
||||
/** @type {[typeof __VLS_components.DropdownMenuItem, typeof __VLS_components.DropdownMenuItem, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_226 = __VLS_asFunctionalComponent(__VLS_225, new __VLS_225({
|
||||
...{ 'onClick': {} },
|
||||
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
|
||||
}));
|
||||
const __VLS_227 = __VLS_226({
|
||||
...{ 'onClick': {} },
|
||||
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_226));
|
||||
let __VLS_229;
|
||||
let __VLS_230;
|
||||
let __VLS_231;
|
||||
const __VLS_232 = {
|
||||
onClick: (__VLS_ctx.markUnread)
|
||||
};
|
||||
__VLS_228.slots.default;
|
||||
const __VLS_233 = {}.MailOpen;
|
||||
/** @type {[typeof __VLS_components.MailOpen, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_234 = __VLS_asFunctionalComponent(__VLS_233, new __VLS_233({
|
||||
...{ class: "mr-2 size-4" },
|
||||
}));
|
||||
const __VLS_235 = __VLS_234({
|
||||
...{ class: "mr-2 size-4" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_234));
|
||||
var __VLS_228;
|
||||
const __VLS_237 = {}.DropdownMenuItem;
|
||||
/** @type {[typeof __VLS_components.DropdownMenuItem, typeof __VLS_components.DropdownMenuItem, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_238 = __VLS_asFunctionalComponent(__VLS_237, new __VLS_237({
|
||||
...{ 'onClick': {} },
|
||||
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
|
||||
}));
|
||||
const __VLS_239 = __VLS_238({
|
||||
...{ 'onClick': {} },
|
||||
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_238));
|
||||
let __VLS_241;
|
||||
let __VLS_242;
|
||||
let __VLS_243;
|
||||
const __VLS_244 = {
|
||||
onClick: (__VLS_ctx.toggleStar)
|
||||
};
|
||||
__VLS_240.slots.default;
|
||||
const __VLS_245 = {}.Star;
|
||||
/** @type {[typeof __VLS_components.Star, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_246 = __VLS_asFunctionalComponent(__VLS_245, new __VLS_245({
|
||||
...{ class: "mr-2 size-4" },
|
||||
}));
|
||||
const __VLS_247 = __VLS_246({
|
||||
...{ class: "mr-2 size-4" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_246));
|
||||
(__VLS_ctx.message?.is_starred ? 'Unstar' : 'Star');
|
||||
var __VLS_240;
|
||||
const __VLS_249 = {}.DropdownMenuSeparator;
|
||||
/** @type {[typeof __VLS_components.DropdownMenuSeparator, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_250 = __VLS_asFunctionalComponent(__VLS_249, new __VLS_249({
|
||||
...{ class: "my-1 h-px bg-border" },
|
||||
...{ 'onClick': {} },
|
||||
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
|
||||
}));
|
||||
const __VLS_251 = __VLS_250({
|
||||
...{ class: "my-1 h-px bg-border" },
|
||||
...{ 'onClick': {} },
|
||||
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_250));
|
||||
const __VLS_253 = {}.DropdownMenuSub;
|
||||
/** @type {[typeof __VLS_components.DropdownMenuSub, typeof __VLS_components.DropdownMenuSub, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_254 = __VLS_asFunctionalComponent(__VLS_253, new __VLS_253({}));
|
||||
const __VLS_255 = __VLS_254({}, ...__VLS_functionalComponentArgsRest(__VLS_254));
|
||||
__VLS_256.slots.default;
|
||||
const __VLS_257 = {}.DropdownMenuSubTrigger;
|
||||
/** @type {[typeof __VLS_components.DropdownMenuSubTrigger, typeof __VLS_components.DropdownMenuSubTrigger, ]} */ ;
|
||||
let __VLS_253;
|
||||
let __VLS_254;
|
||||
let __VLS_255;
|
||||
const __VLS_256 = {
|
||||
onClick: (__VLS_ctx.toggleStar)
|
||||
};
|
||||
__VLS_252.slots.default;
|
||||
const __VLS_257 = {}.Star;
|
||||
/** @type {[typeof __VLS_components.Star, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_258 = __VLS_asFunctionalComponent(__VLS_257, new __VLS_257({
|
||||
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
|
||||
...{ class: "mr-2 size-4" },
|
||||
}));
|
||||
const __VLS_259 = __VLS_258({
|
||||
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
|
||||
...{ class: "mr-2 size-4" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_258));
|
||||
__VLS_260.slots.default;
|
||||
const __VLS_261 = {}.FolderInput;
|
||||
/** @type {[typeof __VLS_components.FolderInput, ]} */ ;
|
||||
(__VLS_ctx.message?.is_starred ? 'Unstar' : 'Star');
|
||||
var __VLS_252;
|
||||
const __VLS_261 = {}.DropdownMenuSeparator;
|
||||
/** @type {[typeof __VLS_components.DropdownMenuSeparator, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_262 = __VLS_asFunctionalComponent(__VLS_261, new __VLS_261({
|
||||
...{ class: "mr-2 size-4" },
|
||||
...{ class: "my-1 h-px bg-border" },
|
||||
}));
|
||||
const __VLS_263 = __VLS_262({
|
||||
...{ class: "mr-2 size-4" },
|
||||
...{ class: "my-1 h-px bg-border" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_262));
|
||||
var __VLS_260;
|
||||
const __VLS_265 = {}.DropdownMenuPortal;
|
||||
/** @type {[typeof __VLS_components.DropdownMenuPortal, typeof __VLS_components.DropdownMenuPortal, ]} */ ;
|
||||
const __VLS_265 = {}.DropdownMenuSub;
|
||||
/** @type {[typeof __VLS_components.DropdownMenuSub, typeof __VLS_components.DropdownMenuSub, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_266 = __VLS_asFunctionalComponent(__VLS_265, new __VLS_265({}));
|
||||
const __VLS_267 = __VLS_266({}, ...__VLS_functionalComponentArgsRest(__VLS_266));
|
||||
__VLS_268.slots.default;
|
||||
const __VLS_269 = {}.DropdownMenuSubContent;
|
||||
/** @type {[typeof __VLS_components.DropdownMenuSubContent, typeof __VLS_components.DropdownMenuSubContent, ]} */ ;
|
||||
const __VLS_269 = {}.DropdownMenuSubTrigger;
|
||||
/** @type {[typeof __VLS_components.DropdownMenuSubTrigger, typeof __VLS_components.DropdownMenuSubTrigger, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_270 = __VLS_asFunctionalComponent(__VLS_269, new __VLS_269({
|
||||
...{ class: "z-50 min-w-[140px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md" },
|
||||
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
|
||||
}));
|
||||
const __VLS_271 = __VLS_270({
|
||||
...{ class: "z-50 min-w-[140px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md" },
|
||||
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_270));
|
||||
__VLS_272.slots.default;
|
||||
const __VLS_273 = {}.FolderInput;
|
||||
/** @type {[typeof __VLS_components.FolderInput, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_274 = __VLS_asFunctionalComponent(__VLS_273, new __VLS_273({
|
||||
...{ class: "mr-2 size-4" },
|
||||
}));
|
||||
const __VLS_275 = __VLS_274({
|
||||
...{ class: "mr-2 size-4" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_274));
|
||||
var __VLS_272;
|
||||
const __VLS_277 = {}.DropdownMenuPortal;
|
||||
/** @type {[typeof __VLS_components.DropdownMenuPortal, typeof __VLS_components.DropdownMenuPortal, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_278 = __VLS_asFunctionalComponent(__VLS_277, new __VLS_277({}));
|
||||
const __VLS_279 = __VLS_278({}, ...__VLS_functionalComponentArgsRest(__VLS_278));
|
||||
__VLS_280.slots.default;
|
||||
const __VLS_281 = {}.DropdownMenuSubContent;
|
||||
/** @type {[typeof __VLS_components.DropdownMenuSubContent, typeof __VLS_components.DropdownMenuSubContent, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_282 = __VLS_asFunctionalComponent(__VLS_281, new __VLS_281({
|
||||
...{ class: "z-50 min-w-[140px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md" },
|
||||
}));
|
||||
const __VLS_283 = __VLS_282({
|
||||
...{ class: "z-50 min-w-[140px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_282));
|
||||
__VLS_284.slots.default;
|
||||
for (const [f] of __VLS_getVForSourceType((__VLS_ctx.otherFolders))) {
|
||||
const __VLS_273 = {}.DropdownMenuItem;
|
||||
const __VLS_285 = {}.DropdownMenuItem;
|
||||
/** @type {[typeof __VLS_components.DropdownMenuItem, typeof __VLS_components.DropdownMenuItem, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_274 = __VLS_asFunctionalComponent(__VLS_273, new __VLS_273({
|
||||
const __VLS_286 = __VLS_asFunctionalComponent(__VLS_285, new __VLS_285({
|
||||
...{ 'onClick': {} },
|
||||
key: (f.id),
|
||||
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
|
||||
}));
|
||||
const __VLS_275 = __VLS_274({
|
||||
const __VLS_287 = __VLS_286({
|
||||
...{ 'onClick': {} },
|
||||
key: (f.id),
|
||||
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_274));
|
||||
let __VLS_277;
|
||||
let __VLS_278;
|
||||
let __VLS_279;
|
||||
const __VLS_280 = {
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_286));
|
||||
let __VLS_289;
|
||||
let __VLS_290;
|
||||
let __VLS_291;
|
||||
const __VLS_292 = {
|
||||
onClick: (...[$event]) => {
|
||||
__VLS_ctx.moveToFolder(f);
|
||||
}
|
||||
};
|
||||
__VLS_276.slots.default;
|
||||
__VLS_288.slots.default;
|
||||
if (f.color) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span)({
|
||||
...{ class: "mr-2 inline-block h-2 w-2 rounded-full flex-shrink-0" },
|
||||
|
|
@ -1020,11 +1112,11 @@ for (const [f] of __VLS_getVForSourceType((__VLS_ctx.otherFolders))) {
|
|||
});
|
||||
}
|
||||
(f.name);
|
||||
var __VLS_276;
|
||||
var __VLS_288;
|
||||
}
|
||||
var __VLS_272;
|
||||
var __VLS_284;
|
||||
var __VLS_280;
|
||||
var __VLS_268;
|
||||
var __VLS_256;
|
||||
var __VLS_224;
|
||||
var __VLS_220;
|
||||
var __VLS_205;
|
||||
|
|
@ -1052,12 +1144,12 @@ if (__VLS_ctx.message) {
|
|||
});
|
||||
/** @type {[typeof Avatar, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_281 = __VLS_asFunctionalComponent(Avatar, new Avatar({
|
||||
const __VLS_293 = __VLS_asFunctionalComponent(Avatar, new Avatar({
|
||||
initials: (__VLS_ctx.message.from_name?.[0] || __VLS_ctx.message.from_address?.[0] || '?'),
|
||||
}));
|
||||
const __VLS_282 = __VLS_281({
|
||||
const __VLS_294 = __VLS_293({
|
||||
initials: (__VLS_ctx.message.from_name?.[0] || __VLS_ctx.message.from_address?.[0] || '?'),
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_281));
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_293));
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "grid gap-1" },
|
||||
});
|
||||
|
|
@ -1065,17 +1157,37 @@ if (__VLS_ctx.message) {
|
|||
...{ class: "font-semibold" },
|
||||
});
|
||||
(__VLS_ctx.message.from_name || __VLS_ctx.message.from_address);
|
||||
if (__VLS_ctx.toDisplay) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "line-clamp-1 text-xs" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
|
||||
...{ class: "font-medium" },
|
||||
});
|
||||
(__VLS_ctx.toDisplay);
|
||||
}
|
||||
if (__VLS_ctx.ccDisplay) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "line-clamp-1 text-xs" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
|
||||
...{ class: "font-medium" },
|
||||
});
|
||||
(__VLS_ctx.ccDisplay);
|
||||
}
|
||||
if (__VLS_ctx.bccDisplay) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "line-clamp-1 text-xs" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
|
||||
...{ class: "font-medium" },
|
||||
});
|
||||
(__VLS_ctx.bccDisplay);
|
||||
}
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "line-clamp-1 text-xs" },
|
||||
});
|
||||
(__VLS_ctx.message.subject);
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "line-clamp-1 text-xs" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
|
||||
...{ class: "font-medium" },
|
||||
});
|
||||
(__VLS_ctx.message.from_address);
|
||||
if (__VLS_ctx.displayTimestamp) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "ml-auto text-xs text-muted-foreground" },
|
||||
|
|
@ -1084,8 +1196,8 @@ if (__VLS_ctx.message) {
|
|||
}
|
||||
/** @type {[typeof Separator, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_284 = __VLS_asFunctionalComponent(Separator, new Separator({}));
|
||||
const __VLS_285 = __VLS_284({}, ...__VLS_functionalComponentArgsRest(__VLS_284));
|
||||
const __VLS_296 = __VLS_asFunctionalComponent(Separator, new Separator({}));
|
||||
const __VLS_297 = __VLS_296({}, ...__VLS_functionalComponentArgsRest(__VLS_296));
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "flex-1 overflow-y-auto" },
|
||||
});
|
||||
|
|
@ -1109,20 +1221,20 @@ if (__VLS_ctx.message) {
|
|||
}
|
||||
/** @type {[typeof AttachmentBar, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_287 = __VLS_asFunctionalComponent(AttachmentBar, new AttachmentBar({
|
||||
const __VLS_299 = __VLS_asFunctionalComponent(AttachmentBar, new AttachmentBar({
|
||||
attachments: (__VLS_ctx.message.attachments),
|
||||
}));
|
||||
const __VLS_288 = __VLS_287({
|
||||
const __VLS_300 = __VLS_299({
|
||||
attachments: (__VLS_ctx.message.attachments),
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_287));
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_299));
|
||||
/** @type {[typeof Separator, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_290 = __VLS_asFunctionalComponent(Separator, new Separator({
|
||||
const __VLS_302 = __VLS_asFunctionalComponent(Separator, new Separator({
|
||||
...{ class: "mt-auto print-hide" },
|
||||
}));
|
||||
const __VLS_291 = __VLS_290({
|
||||
const __VLS_303 = __VLS_302({
|
||||
...{ class: "mt-auto print-hide" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_290));
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_302));
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "p-4 print-hide" },
|
||||
});
|
||||
|
|
@ -1134,36 +1246,36 @@ if (__VLS_ctx.message) {
|
|||
});
|
||||
/** @type {[typeof Textarea, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_293 = __VLS_asFunctionalComponent(Textarea, new Textarea({
|
||||
const __VLS_305 = __VLS_asFunctionalComponent(Textarea, new Textarea({
|
||||
modelValue: (__VLS_ctx.replyText),
|
||||
...{ class: "p-4 min-h-[100px]" },
|
||||
placeholder: (`Reply ${__VLS_ctx.message.from_name || __VLS_ctx.message.from_address}...`),
|
||||
}));
|
||||
const __VLS_294 = __VLS_293({
|
||||
const __VLS_306 = __VLS_305({
|
||||
modelValue: (__VLS_ctx.replyText),
|
||||
...{ class: "p-4 min-h-[100px]" },
|
||||
placeholder: (`Reply ${__VLS_ctx.message.from_name || __VLS_ctx.message.from_address}...`),
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_293));
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_305));
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "flex items-center" },
|
||||
});
|
||||
/** @type {[typeof Button, typeof Button, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_296 = __VLS_asFunctionalComponent(Button, new Button({
|
||||
const __VLS_308 = __VLS_asFunctionalComponent(Button, new Button({
|
||||
type: "submit",
|
||||
size: "sm",
|
||||
...{ class: "ml-auto" },
|
||||
disabled: (__VLS_ctx.sendingReply || !__VLS_ctx.replyText.trim()),
|
||||
}));
|
||||
const __VLS_297 = __VLS_296({
|
||||
const __VLS_309 = __VLS_308({
|
||||
type: "submit",
|
||||
size: "sm",
|
||||
...{ class: "ml-auto" },
|
||||
disabled: (__VLS_ctx.sendingReply || !__VLS_ctx.replyText.trim()),
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_296));
|
||||
__VLS_298.slots.default;
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_308));
|
||||
__VLS_310.slots.default;
|
||||
(__VLS_ctx.sendingReply ? 'Sending...' : 'Send');
|
||||
var __VLS_298;
|
||||
var __VLS_310;
|
||||
}
|
||||
else {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
|
|
@ -1298,6 +1410,19 @@ else {
|
|||
/** @type {__VLS_StyleScopedClasses['hover:bg-accent']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['mr-2']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['size-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['relative']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['cursor-pointer']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['select-none']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['rounded-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['px-2']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['py-1.5']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['hover:bg-accent']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['mr-2']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['size-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['my-1']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['h-px']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['bg-border']} */ ;
|
||||
|
|
@ -1363,9 +1488,15 @@ else {
|
|||
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['line-clamp-1']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['line-clamp-1']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['line-clamp-1']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['line-clamp-1']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['ml-auto']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
|
|
@ -1434,6 +1565,9 @@ const __VLS_self = (await import('vue')).defineComponent({
|
|||
emailIframe: emailIframe,
|
||||
safeHtml: safeHtml,
|
||||
resizeIframe: resizeIframe,
|
||||
toDisplay: toDisplay,
|
||||
ccDisplay: ccDisplay,
|
||||
bccDisplay: bccDisplay,
|
||||
displayTimestamp: displayTimestamp,
|
||||
otherFolders: otherFolders,
|
||||
replyTo: replyTo,
|
||||
|
|
@ -1442,6 +1576,7 @@ const __VLS_self = (await import('vue')).defineComponent({
|
|||
backToList: backToList,
|
||||
trash: trash,
|
||||
markUnread: markUnread,
|
||||
markRead: markRead,
|
||||
toggleStar: toggleStar,
|
||||
moveToFolder: moveToFolder,
|
||||
printMessage: printMessage,
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,31 +1,21 @@
|
|||
/// <reference types="../../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
|
||||
import { ref, computed } from 'vue';
|
||||
import { Search, ArrowDownUp, Trash2 } from 'lucide-vue-next';
|
||||
import { ArrowDownUp, Trash2 } from 'lucide-vue-next';
|
||||
import { TabsRoot, TabsList, TabsTrigger, TabsContent, } from 'radix-vue';
|
||||
import { ScrollAreaRoot, ScrollAreaViewport, ScrollAreaScrollbar, ScrollAreaThumb, } from 'radix-vue';
|
||||
import { useMailStore } from '../../stores/mail';
|
||||
import { mailApi } from '../../api/mail';
|
||||
import MessageListItem from './MessageListItem.vue';
|
||||
import Input from '../ui/Input.vue';
|
||||
import SearchBar from './SearchBar.vue';
|
||||
import Dialog from '../ui/Dialog.vue';
|
||||
import Button from '../ui/Button.vue';
|
||||
const store = useMailStore();
|
||||
const searchValue = ref('');
|
||||
const showTrashConfirm = ref(false);
|
||||
const folderColor = computed(() => store.currentFolderObj?.color || '');
|
||||
const filteredMessages = computed(() => {
|
||||
const q = searchValue.value.trim().toLowerCase();
|
||||
if (!q)
|
||||
return store.messages;
|
||||
return store.messages.filter((m) => (m.from_name || '').toLowerCase().includes(q) ||
|
||||
(m.from_address || '').toLowerCase().includes(q) ||
|
||||
(m.subject || '').toLowerCase().includes(q) ||
|
||||
(m.text_body || '').toLowerCase().includes(q));
|
||||
});
|
||||
const unreadMessages = computed(() => filteredMessages.value.filter((m) => !m.is_read));
|
||||
const starredMessages = computed(() => filteredMessages.value.filter((m) => m.is_starred));
|
||||
const unreadMessages = computed(() => store.messages.filter((m) => !m.is_read));
|
||||
const starredMessages = computed(() => store.messages.filter((m) => m.is_starred));
|
||||
const displayMessages = computed(() => {
|
||||
let msgs = filteredMessages.value;
|
||||
let msgs = store.messages;
|
||||
if (store.activeTab === 'unread')
|
||||
msgs = unreadMessages.value;
|
||||
else if (store.activeTab === 'starred')
|
||||
|
|
@ -68,17 +58,20 @@ const emptyTrash = () => {
|
|||
}
|
||||
};
|
||||
const performEmptyTrash = async () => {
|
||||
if (store.currentFolderObj) {
|
||||
try {
|
||||
await mailApi.emptyFolder(store.currentMailbox, store.currentFolderObj.id);
|
||||
store.messages = [];
|
||||
store.currentMessage = null;
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
finally {
|
||||
showTrashConfirm.value = false;
|
||||
}
|
||||
if (!store.currentFolderObj)
|
||||
return;
|
||||
try {
|
||||
await mailApi.emptyFolder(store.currentMailbox, store.currentFolderObj.id);
|
||||
store.messages = [];
|
||||
store.currentMessage = null;
|
||||
const fRes = await mailApi.getFolders(store.currentMailbox);
|
||||
store.folders = fRes.data;
|
||||
}
|
||||
catch {
|
||||
store.showToast('Failed to empty trash');
|
||||
}
|
||||
finally {
|
||||
showTrashConfirm.value = false;
|
||||
}
|
||||
};
|
||||
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
|
||||
|
|
@ -195,44 +188,34 @@ var __VLS_15;
|
|||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "relative" },
|
||||
});
|
||||
const __VLS_28 = {}.Search;
|
||||
/** @type {[typeof __VLS_components.Search, ]} */ ;
|
||||
/** @type {[typeof SearchBar, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_29 = __VLS_asFunctionalComponent(__VLS_28, new __VLS_28({
|
||||
...{ class: "absolute left-2 top-2.5 size-4 text-muted-foreground" },
|
||||
}));
|
||||
const __VLS_30 = __VLS_29({
|
||||
...{ class: "absolute left-2 top-2.5 size-4 text-muted-foreground" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_29));
|
||||
/** @type {[typeof Input, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_32 = __VLS_asFunctionalComponent(Input, new Input({
|
||||
modelValue: (__VLS_ctx.searchValue),
|
||||
placeholder: "Search",
|
||||
...{ class: "pl-8" },
|
||||
}));
|
||||
const __VLS_33 = __VLS_32({
|
||||
modelValue: (__VLS_ctx.searchValue),
|
||||
placeholder: "Search",
|
||||
...{ class: "pl-8" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_32));
|
||||
const __VLS_35 = {}.TabsContent;
|
||||
const __VLS_28 = __VLS_asFunctionalComponent(SearchBar, new SearchBar({}));
|
||||
const __VLS_29 = __VLS_28({}, ...__VLS_functionalComponentArgsRest(__VLS_28));
|
||||
const __VLS_31 = {}.TabsContent;
|
||||
/** @type {[typeof __VLS_components.TabsContent, typeof __VLS_components.TabsContent, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_36 = __VLS_asFunctionalComponent(__VLS_35, new __VLS_35({
|
||||
const __VLS_32 = __VLS_asFunctionalComponent(__VLS_31, new __VLS_31({
|
||||
value: "all",
|
||||
...{ class: "m-0 flex-1 overflow-hidden" },
|
||||
}));
|
||||
const __VLS_37 = __VLS_36({
|
||||
const __VLS_33 = __VLS_32({
|
||||
value: "all",
|
||||
...{ class: "m-0 flex-1 overflow-hidden" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_32));
|
||||
__VLS_34.slots.default;
|
||||
const __VLS_35 = {}.ScrollAreaRoot;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_36 = __VLS_asFunctionalComponent(__VLS_35, new __VLS_35({
|
||||
...{ class: "h-full" },
|
||||
}));
|
||||
const __VLS_37 = __VLS_36({
|
||||
...{ class: "h-full" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_36));
|
||||
__VLS_38.slots.default;
|
||||
const __VLS_39 = {}.ScrollAreaRoot;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
|
||||
const __VLS_39 = {}.ScrollAreaViewport;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_40 = __VLS_asFunctionalComponent(__VLS_39, new __VLS_39({
|
||||
...{ class: "h-full" },
|
||||
|
|
@ -241,103 +224,129 @@ const __VLS_41 = __VLS_40({
|
|||
...{ class: "h-full" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_40));
|
||||
__VLS_42.slots.default;
|
||||
const __VLS_43 = {}.ScrollAreaViewport;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_44 = __VLS_asFunctionalComponent(__VLS_43, new __VLS_43({
|
||||
...{ class: "h-full" },
|
||||
}));
|
||||
const __VLS_45 = __VLS_44({
|
||||
...{ class: "h-full" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_44));
|
||||
__VLS_46.slots.default;
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "flex flex-col gap-2 p-4 pt-0" },
|
||||
});
|
||||
const __VLS_47 = {}.TransitionGroup;
|
||||
/** @type {[typeof __VLS_components.TransitionGroup, typeof __VLS_components.TransitionGroup, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_48 = __VLS_asFunctionalComponent(__VLS_47, new __VLS_47({
|
||||
name: "list",
|
||||
appear: true,
|
||||
}));
|
||||
const __VLS_49 = __VLS_48({
|
||||
name: "list",
|
||||
appear: true,
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_48));
|
||||
__VLS_50.slots.default;
|
||||
for (const [msg] of __VLS_getVForSourceType((__VLS_ctx.displayMessages))) {
|
||||
/** @type {[typeof MessageListItem, ]} */ ;
|
||||
if (__VLS_ctx.store.messagesLoading) {
|
||||
for (const [n] of __VLS_getVForSourceType((6))) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div)({
|
||||
key: (n),
|
||||
...{ class: "h-16 rounded-lg bg-muted animate-pulse" },
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
const __VLS_43 = {}.TransitionGroup;
|
||||
/** @type {[typeof __VLS_components.TransitionGroup, typeof __VLS_components.TransitionGroup, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_51 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
|
||||
...{ 'onClick': {} },
|
||||
key: (msg.id),
|
||||
message: (msg),
|
||||
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
|
||||
folderColor: (__VLS_ctx.folderColor),
|
||||
const __VLS_44 = __VLS_asFunctionalComponent(__VLS_43, new __VLS_43({
|
||||
name: "list",
|
||||
appear: true,
|
||||
}));
|
||||
const __VLS_52 = __VLS_51({
|
||||
...{ 'onClick': {} },
|
||||
key: (msg.id),
|
||||
message: (msg),
|
||||
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
|
||||
folderColor: (__VLS_ctx.folderColor),
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_51));
|
||||
let __VLS_54;
|
||||
let __VLS_55;
|
||||
let __VLS_56;
|
||||
const __VLS_57 = {
|
||||
onClick: (...[$event]) => {
|
||||
__VLS_ctx.selectMessage(msg);
|
||||
}
|
||||
};
|
||||
var __VLS_53;
|
||||
const __VLS_45 = __VLS_44({
|
||||
name: "list",
|
||||
appear: true,
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_44));
|
||||
__VLS_46.slots.default;
|
||||
for (const [msg] of __VLS_getVForSourceType((__VLS_ctx.displayMessages))) {
|
||||
/** @type {[typeof MessageListItem, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_47 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
|
||||
...{ 'onClick': {} },
|
||||
key: (msg.id),
|
||||
message: (msg),
|
||||
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
|
||||
folderColor: (__VLS_ctx.folderColor),
|
||||
}));
|
||||
const __VLS_48 = __VLS_47({
|
||||
...{ 'onClick': {} },
|
||||
key: (msg.id),
|
||||
message: (msg),
|
||||
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
|
||||
folderColor: (__VLS_ctx.folderColor),
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_47));
|
||||
let __VLS_50;
|
||||
let __VLS_51;
|
||||
let __VLS_52;
|
||||
const __VLS_53 = {
|
||||
onClick: (...[$event]) => {
|
||||
if (!!(__VLS_ctx.store.messagesLoading))
|
||||
return;
|
||||
__VLS_ctx.selectMessage(msg);
|
||||
}
|
||||
};
|
||||
var __VLS_49;
|
||||
}
|
||||
var __VLS_46;
|
||||
if (__VLS_ctx.displayMessages.length === 0) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "p-8 text-center text-muted-foreground" },
|
||||
});
|
||||
}
|
||||
if (__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
|
||||
...{ onClick: (...[$event]) => {
|
||||
if (!!(__VLS_ctx.store.messagesLoading))
|
||||
return;
|
||||
if (!(__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading))
|
||||
return;
|
||||
__VLS_ctx.store.loadMore();
|
||||
} },
|
||||
...{ class: "w-full py-2 text-sm text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50" },
|
||||
disabled: (__VLS_ctx.store.isFetchingNextPage),
|
||||
});
|
||||
(__VLS_ctx.store.isFetchingNextPage ? 'Loading…' : 'Load more');
|
||||
}
|
||||
}
|
||||
var __VLS_50;
|
||||
if (__VLS_ctx.displayMessages.length === 0) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "p-8 text-center text-muted-foreground" },
|
||||
});
|
||||
}
|
||||
var __VLS_46;
|
||||
const __VLS_58 = {}.ScrollAreaScrollbar;
|
||||
var __VLS_42;
|
||||
const __VLS_54 = {}.ScrollAreaScrollbar;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaScrollbar, typeof __VLS_components.ScrollAreaScrollbar, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_59 = __VLS_asFunctionalComponent(__VLS_58, new __VLS_58({
|
||||
const __VLS_55 = __VLS_asFunctionalComponent(__VLS_54, new __VLS_54({
|
||||
orientation: "vertical",
|
||||
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
|
||||
}));
|
||||
const __VLS_60 = __VLS_59({
|
||||
const __VLS_56 = __VLS_55({
|
||||
orientation: "vertical",
|
||||
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_59));
|
||||
__VLS_61.slots.default;
|
||||
const __VLS_62 = {}.ScrollAreaThumb;
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_55));
|
||||
__VLS_57.slots.default;
|
||||
const __VLS_58 = {}.ScrollAreaThumb;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaThumb, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_63 = __VLS_asFunctionalComponent(__VLS_62, new __VLS_62({
|
||||
const __VLS_59 = __VLS_asFunctionalComponent(__VLS_58, new __VLS_58({
|
||||
...{ class: "relative flex-1 rounded-full bg-border" },
|
||||
}));
|
||||
const __VLS_64 = __VLS_63({
|
||||
const __VLS_60 = __VLS_59({
|
||||
...{ class: "relative flex-1 rounded-full bg-border" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_63));
|
||||
var __VLS_61;
|
||||
var __VLS_42;
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_59));
|
||||
var __VLS_57;
|
||||
var __VLS_38;
|
||||
const __VLS_66 = {}.TabsContent;
|
||||
var __VLS_34;
|
||||
const __VLS_62 = {}.TabsContent;
|
||||
/** @type {[typeof __VLS_components.TabsContent, typeof __VLS_components.TabsContent, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_67 = __VLS_asFunctionalComponent(__VLS_66, new __VLS_66({
|
||||
const __VLS_63 = __VLS_asFunctionalComponent(__VLS_62, new __VLS_62({
|
||||
value: "unread",
|
||||
...{ class: "m-0 flex-1 overflow-hidden" },
|
||||
}));
|
||||
const __VLS_68 = __VLS_67({
|
||||
const __VLS_64 = __VLS_63({
|
||||
value: "unread",
|
||||
...{ class: "m-0 flex-1 overflow-hidden" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_63));
|
||||
__VLS_65.slots.default;
|
||||
const __VLS_66 = {}.ScrollAreaRoot;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_67 = __VLS_asFunctionalComponent(__VLS_66, new __VLS_66({
|
||||
...{ class: "h-full" },
|
||||
}));
|
||||
const __VLS_68 = __VLS_67({
|
||||
...{ class: "h-full" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_67));
|
||||
__VLS_69.slots.default;
|
||||
const __VLS_70 = {}.ScrollAreaRoot;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
|
||||
const __VLS_70 = {}.ScrollAreaViewport;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_71 = __VLS_asFunctionalComponent(__VLS_70, new __VLS_70({
|
||||
...{ class: "h-full" },
|
||||
|
|
@ -346,103 +355,129 @@ const __VLS_72 = __VLS_71({
|
|||
...{ class: "h-full" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_71));
|
||||
__VLS_73.slots.default;
|
||||
const __VLS_74 = {}.ScrollAreaViewport;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_75 = __VLS_asFunctionalComponent(__VLS_74, new __VLS_74({
|
||||
...{ class: "h-full" },
|
||||
}));
|
||||
const __VLS_76 = __VLS_75({
|
||||
...{ class: "h-full" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_75));
|
||||
__VLS_77.slots.default;
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "flex flex-col gap-2 p-4 pt-0" },
|
||||
});
|
||||
const __VLS_78 = {}.TransitionGroup;
|
||||
/** @type {[typeof __VLS_components.TransitionGroup, typeof __VLS_components.TransitionGroup, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_79 = __VLS_asFunctionalComponent(__VLS_78, new __VLS_78({
|
||||
name: "list",
|
||||
appear: true,
|
||||
}));
|
||||
const __VLS_80 = __VLS_79({
|
||||
name: "list",
|
||||
appear: true,
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_79));
|
||||
__VLS_81.slots.default;
|
||||
for (const [msg] of __VLS_getVForSourceType((__VLS_ctx.displayMessages))) {
|
||||
/** @type {[typeof MessageListItem, ]} */ ;
|
||||
if (__VLS_ctx.store.messagesLoading) {
|
||||
for (const [n] of __VLS_getVForSourceType((6))) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div)({
|
||||
key: (n),
|
||||
...{ class: "h-16 rounded-lg bg-muted animate-pulse" },
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
const __VLS_74 = {}.TransitionGroup;
|
||||
/** @type {[typeof __VLS_components.TransitionGroup, typeof __VLS_components.TransitionGroup, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_82 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
|
||||
...{ 'onClick': {} },
|
||||
key: (msg.id),
|
||||
message: (msg),
|
||||
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
|
||||
folderColor: (__VLS_ctx.folderColor),
|
||||
const __VLS_75 = __VLS_asFunctionalComponent(__VLS_74, new __VLS_74({
|
||||
name: "list",
|
||||
appear: true,
|
||||
}));
|
||||
const __VLS_83 = __VLS_82({
|
||||
...{ 'onClick': {} },
|
||||
key: (msg.id),
|
||||
message: (msg),
|
||||
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
|
||||
folderColor: (__VLS_ctx.folderColor),
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_82));
|
||||
let __VLS_85;
|
||||
let __VLS_86;
|
||||
let __VLS_87;
|
||||
const __VLS_88 = {
|
||||
onClick: (...[$event]) => {
|
||||
__VLS_ctx.selectMessage(msg);
|
||||
}
|
||||
};
|
||||
var __VLS_84;
|
||||
const __VLS_76 = __VLS_75({
|
||||
name: "list",
|
||||
appear: true,
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_75));
|
||||
__VLS_77.slots.default;
|
||||
for (const [msg] of __VLS_getVForSourceType((__VLS_ctx.displayMessages))) {
|
||||
/** @type {[typeof MessageListItem, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_78 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
|
||||
...{ 'onClick': {} },
|
||||
key: (msg.id),
|
||||
message: (msg),
|
||||
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
|
||||
folderColor: (__VLS_ctx.folderColor),
|
||||
}));
|
||||
const __VLS_79 = __VLS_78({
|
||||
...{ 'onClick': {} },
|
||||
key: (msg.id),
|
||||
message: (msg),
|
||||
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
|
||||
folderColor: (__VLS_ctx.folderColor),
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_78));
|
||||
let __VLS_81;
|
||||
let __VLS_82;
|
||||
let __VLS_83;
|
||||
const __VLS_84 = {
|
||||
onClick: (...[$event]) => {
|
||||
if (!!(__VLS_ctx.store.messagesLoading))
|
||||
return;
|
||||
__VLS_ctx.selectMessage(msg);
|
||||
}
|
||||
};
|
||||
var __VLS_80;
|
||||
}
|
||||
var __VLS_77;
|
||||
if (__VLS_ctx.displayMessages.length === 0) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "p-8 text-center text-muted-foreground" },
|
||||
});
|
||||
}
|
||||
if (__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
|
||||
...{ onClick: (...[$event]) => {
|
||||
if (!!(__VLS_ctx.store.messagesLoading))
|
||||
return;
|
||||
if (!(__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading))
|
||||
return;
|
||||
__VLS_ctx.store.loadMore();
|
||||
} },
|
||||
...{ class: "w-full py-2 text-sm text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50" },
|
||||
disabled: (__VLS_ctx.store.isFetchingNextPage),
|
||||
});
|
||||
(__VLS_ctx.store.isFetchingNextPage ? 'Loading…' : 'Load more');
|
||||
}
|
||||
}
|
||||
var __VLS_81;
|
||||
if (__VLS_ctx.displayMessages.length === 0) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "p-8 text-center text-muted-foreground" },
|
||||
});
|
||||
}
|
||||
var __VLS_77;
|
||||
const __VLS_89 = {}.ScrollAreaScrollbar;
|
||||
var __VLS_73;
|
||||
const __VLS_85 = {}.ScrollAreaScrollbar;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaScrollbar, typeof __VLS_components.ScrollAreaScrollbar, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_90 = __VLS_asFunctionalComponent(__VLS_89, new __VLS_89({
|
||||
const __VLS_86 = __VLS_asFunctionalComponent(__VLS_85, new __VLS_85({
|
||||
orientation: "vertical",
|
||||
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
|
||||
}));
|
||||
const __VLS_91 = __VLS_90({
|
||||
const __VLS_87 = __VLS_86({
|
||||
orientation: "vertical",
|
||||
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_90));
|
||||
__VLS_92.slots.default;
|
||||
const __VLS_93 = {}.ScrollAreaThumb;
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_86));
|
||||
__VLS_88.slots.default;
|
||||
const __VLS_89 = {}.ScrollAreaThumb;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaThumb, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_94 = __VLS_asFunctionalComponent(__VLS_93, new __VLS_93({
|
||||
const __VLS_90 = __VLS_asFunctionalComponent(__VLS_89, new __VLS_89({
|
||||
...{ class: "relative flex-1 rounded-full bg-border" },
|
||||
}));
|
||||
const __VLS_95 = __VLS_94({
|
||||
const __VLS_91 = __VLS_90({
|
||||
...{ class: "relative flex-1 rounded-full bg-border" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_94));
|
||||
var __VLS_92;
|
||||
var __VLS_73;
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_90));
|
||||
var __VLS_88;
|
||||
var __VLS_69;
|
||||
const __VLS_97 = {}.TabsContent;
|
||||
var __VLS_65;
|
||||
const __VLS_93 = {}.TabsContent;
|
||||
/** @type {[typeof __VLS_components.TabsContent, typeof __VLS_components.TabsContent, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_98 = __VLS_asFunctionalComponent(__VLS_97, new __VLS_97({
|
||||
const __VLS_94 = __VLS_asFunctionalComponent(__VLS_93, new __VLS_93({
|
||||
value: "starred",
|
||||
...{ class: "m-0 flex-1 overflow-hidden" },
|
||||
}));
|
||||
const __VLS_99 = __VLS_98({
|
||||
const __VLS_95 = __VLS_94({
|
||||
value: "starred",
|
||||
...{ class: "m-0 flex-1 overflow-hidden" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_94));
|
||||
__VLS_96.slots.default;
|
||||
const __VLS_97 = {}.ScrollAreaRoot;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_98 = __VLS_asFunctionalComponent(__VLS_97, new __VLS_97({
|
||||
...{ class: "h-full" },
|
||||
}));
|
||||
const __VLS_99 = __VLS_98({
|
||||
...{ class: "h-full" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_98));
|
||||
__VLS_100.slots.default;
|
||||
const __VLS_101 = {}.ScrollAreaRoot;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
|
||||
const __VLS_101 = {}.ScrollAreaViewport;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_102 = __VLS_asFunctionalComponent(__VLS_101, new __VLS_101({
|
||||
...{ class: "h-full" },
|
||||
|
|
@ -451,99 +486,115 @@ const __VLS_103 = __VLS_102({
|
|||
...{ class: "h-full" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_102));
|
||||
__VLS_104.slots.default;
|
||||
const __VLS_105 = {}.ScrollAreaViewport;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_106 = __VLS_asFunctionalComponent(__VLS_105, new __VLS_105({
|
||||
...{ class: "h-full" },
|
||||
}));
|
||||
const __VLS_107 = __VLS_106({
|
||||
...{ class: "h-full" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_106));
|
||||
__VLS_108.slots.default;
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "flex flex-col gap-2 p-4 pt-0" },
|
||||
});
|
||||
const __VLS_109 = {}.TransitionGroup;
|
||||
/** @type {[typeof __VLS_components.TransitionGroup, typeof __VLS_components.TransitionGroup, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_110 = __VLS_asFunctionalComponent(__VLS_109, new __VLS_109({
|
||||
name: "list",
|
||||
appear: true,
|
||||
}));
|
||||
const __VLS_111 = __VLS_110({
|
||||
name: "list",
|
||||
appear: true,
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_110));
|
||||
__VLS_112.slots.default;
|
||||
for (const [msg] of __VLS_getVForSourceType((__VLS_ctx.displayMessages))) {
|
||||
/** @type {[typeof MessageListItem, ]} */ ;
|
||||
if (__VLS_ctx.store.messagesLoading) {
|
||||
for (const [n] of __VLS_getVForSourceType((6))) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div)({
|
||||
key: (n),
|
||||
...{ class: "h-16 rounded-lg bg-muted animate-pulse" },
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
const __VLS_105 = {}.TransitionGroup;
|
||||
/** @type {[typeof __VLS_components.TransitionGroup, typeof __VLS_components.TransitionGroup, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_113 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
|
||||
...{ 'onClick': {} },
|
||||
key: (msg.id),
|
||||
message: (msg),
|
||||
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
|
||||
folderColor: (__VLS_ctx.folderColor),
|
||||
const __VLS_106 = __VLS_asFunctionalComponent(__VLS_105, new __VLS_105({
|
||||
name: "list",
|
||||
appear: true,
|
||||
}));
|
||||
const __VLS_114 = __VLS_113({
|
||||
...{ 'onClick': {} },
|
||||
key: (msg.id),
|
||||
message: (msg),
|
||||
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
|
||||
folderColor: (__VLS_ctx.folderColor),
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_113));
|
||||
let __VLS_116;
|
||||
let __VLS_117;
|
||||
let __VLS_118;
|
||||
const __VLS_119 = {
|
||||
onClick: (...[$event]) => {
|
||||
__VLS_ctx.selectMessage(msg);
|
||||
}
|
||||
};
|
||||
var __VLS_115;
|
||||
const __VLS_107 = __VLS_106({
|
||||
name: "list",
|
||||
appear: true,
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_106));
|
||||
__VLS_108.slots.default;
|
||||
for (const [msg] of __VLS_getVForSourceType((__VLS_ctx.displayMessages))) {
|
||||
/** @type {[typeof MessageListItem, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_109 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
|
||||
...{ 'onClick': {} },
|
||||
key: (msg.id),
|
||||
message: (msg),
|
||||
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
|
||||
folderColor: (__VLS_ctx.folderColor),
|
||||
}));
|
||||
const __VLS_110 = __VLS_109({
|
||||
...{ 'onClick': {} },
|
||||
key: (msg.id),
|
||||
message: (msg),
|
||||
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
|
||||
folderColor: (__VLS_ctx.folderColor),
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_109));
|
||||
let __VLS_112;
|
||||
let __VLS_113;
|
||||
let __VLS_114;
|
||||
const __VLS_115 = {
|
||||
onClick: (...[$event]) => {
|
||||
if (!!(__VLS_ctx.store.messagesLoading))
|
||||
return;
|
||||
__VLS_ctx.selectMessage(msg);
|
||||
}
|
||||
};
|
||||
var __VLS_111;
|
||||
}
|
||||
var __VLS_108;
|
||||
if (__VLS_ctx.displayMessages.length === 0) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "p-8 text-center text-muted-foreground" },
|
||||
});
|
||||
}
|
||||
if (__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
|
||||
...{ onClick: (...[$event]) => {
|
||||
if (!!(__VLS_ctx.store.messagesLoading))
|
||||
return;
|
||||
if (!(__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading))
|
||||
return;
|
||||
__VLS_ctx.store.loadMore();
|
||||
} },
|
||||
...{ class: "w-full py-2 text-sm text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50" },
|
||||
disabled: (__VLS_ctx.store.isFetchingNextPage),
|
||||
});
|
||||
(__VLS_ctx.store.isFetchingNextPage ? 'Loading…' : 'Load more');
|
||||
}
|
||||
}
|
||||
var __VLS_112;
|
||||
if (__VLS_ctx.displayMessages.length === 0) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "p-8 text-center text-muted-foreground" },
|
||||
});
|
||||
}
|
||||
var __VLS_108;
|
||||
const __VLS_120 = {}.ScrollAreaScrollbar;
|
||||
var __VLS_104;
|
||||
const __VLS_116 = {}.ScrollAreaScrollbar;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaScrollbar, typeof __VLS_components.ScrollAreaScrollbar, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_121 = __VLS_asFunctionalComponent(__VLS_120, new __VLS_120({
|
||||
const __VLS_117 = __VLS_asFunctionalComponent(__VLS_116, new __VLS_116({
|
||||
orientation: "vertical",
|
||||
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
|
||||
}));
|
||||
const __VLS_122 = __VLS_121({
|
||||
const __VLS_118 = __VLS_117({
|
||||
orientation: "vertical",
|
||||
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_121));
|
||||
__VLS_123.slots.default;
|
||||
const __VLS_124 = {}.ScrollAreaThumb;
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_117));
|
||||
__VLS_119.slots.default;
|
||||
const __VLS_120 = {}.ScrollAreaThumb;
|
||||
/** @type {[typeof __VLS_components.ScrollAreaThumb, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_125 = __VLS_asFunctionalComponent(__VLS_124, new __VLS_124({
|
||||
const __VLS_121 = __VLS_asFunctionalComponent(__VLS_120, new __VLS_120({
|
||||
...{ class: "relative flex-1 rounded-full bg-border" },
|
||||
}));
|
||||
const __VLS_126 = __VLS_125({
|
||||
const __VLS_122 = __VLS_121({
|
||||
...{ class: "relative flex-1 rounded-full bg-border" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_125));
|
||||
var __VLS_123;
|
||||
var __VLS_104;
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_121));
|
||||
var __VLS_119;
|
||||
var __VLS_100;
|
||||
var __VLS_96;
|
||||
var __VLS_3;
|
||||
/** @type {[typeof Dialog, typeof Dialog, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_128 = __VLS_asFunctionalComponent(Dialog, new Dialog({
|
||||
const __VLS_124 = __VLS_asFunctionalComponent(Dialog, new Dialog({
|
||||
open: (__VLS_ctx.showTrashConfirm),
|
||||
}));
|
||||
const __VLS_129 = __VLS_128({
|
||||
const __VLS_125 = __VLS_124({
|
||||
open: (__VLS_ctx.showTrashConfirm),
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_128));
|
||||
__VLS_130.slots.default;
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_124));
|
||||
__VLS_126.slots.default;
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "space-y-4" },
|
||||
});
|
||||
|
|
@ -558,43 +609,43 @@ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.d
|
|||
});
|
||||
/** @type {[typeof Button, typeof Button, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_131 = __VLS_asFunctionalComponent(Button, new Button({
|
||||
const __VLS_127 = __VLS_asFunctionalComponent(Button, new Button({
|
||||
...{ 'onClick': {} },
|
||||
variant: "outline",
|
||||
}));
|
||||
const __VLS_132 = __VLS_131({
|
||||
const __VLS_128 = __VLS_127({
|
||||
...{ 'onClick': {} },
|
||||
variant: "outline",
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_131));
|
||||
let __VLS_134;
|
||||
let __VLS_135;
|
||||
let __VLS_136;
|
||||
const __VLS_137 = {
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_127));
|
||||
let __VLS_130;
|
||||
let __VLS_131;
|
||||
let __VLS_132;
|
||||
const __VLS_133 = {
|
||||
onClick: (...[$event]) => {
|
||||
__VLS_ctx.showTrashConfirm = false;
|
||||
}
|
||||
};
|
||||
__VLS_133.slots.default;
|
||||
var __VLS_133;
|
||||
__VLS_129.slots.default;
|
||||
var __VLS_129;
|
||||
/** @type {[typeof Button, typeof Button, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_138 = __VLS_asFunctionalComponent(Button, new Button({
|
||||
const __VLS_134 = __VLS_asFunctionalComponent(Button, new Button({
|
||||
...{ 'onClick': {} },
|
||||
variant: "destructive",
|
||||
}));
|
||||
const __VLS_139 = __VLS_138({
|
||||
const __VLS_135 = __VLS_134({
|
||||
...{ 'onClick': {} },
|
||||
variant: "destructive",
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_138));
|
||||
let __VLS_141;
|
||||
let __VLS_142;
|
||||
let __VLS_143;
|
||||
const __VLS_144 = {
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_134));
|
||||
let __VLS_137;
|
||||
let __VLS_138;
|
||||
let __VLS_139;
|
||||
const __VLS_140 = {
|
||||
onClick: (__VLS_ctx.performEmptyTrash)
|
||||
};
|
||||
__VLS_140.slots.default;
|
||||
var __VLS_140;
|
||||
var __VLS_130;
|
||||
__VLS_136.slots.default;
|
||||
var __VLS_136;
|
||||
var __VLS_126;
|
||||
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['h-full']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['flex-col']} */ ;
|
||||
|
|
@ -697,13 +748,6 @@ var __VLS_130;
|
|||
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['backdrop-blur']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['supports-[backdrop-filter]:bg-background/60']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['relative']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['absolute']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['left-2']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['top-2.5']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['size-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['pl-8']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['m-0']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['flex-1']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['overflow-hidden']} */ ;
|
||||
|
|
@ -714,9 +758,20 @@ var __VLS_130;
|
|||
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['pt-0']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['h-16']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['bg-muted']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['animate-pulse']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['p-8']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['hover:text-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['disabled:opacity-50']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['touch-none']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['select-none']} */ ;
|
||||
|
|
@ -738,9 +793,20 @@ var __VLS_130;
|
|||
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['pt-0']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['h-16']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['bg-muted']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['animate-pulse']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['p-8']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['hover:text-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['disabled:opacity-50']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['touch-none']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['select-none']} */ ;
|
||||
|
|
@ -762,9 +828,20 @@ var __VLS_130;
|
|||
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['pt-0']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['h-16']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['bg-muted']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['animate-pulse']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['p-8']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['hover:text-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['disabled:opacity-50']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['touch-none']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['select-none']} */ ;
|
||||
|
|
@ -791,7 +868,6 @@ var __VLS_dollars;
|
|||
const __VLS_self = (await import('vue')).defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
Search: Search,
|
||||
ArrowDownUp: ArrowDownUp,
|
||||
Trash2: Trash2,
|
||||
TabsRoot: TabsRoot,
|
||||
|
|
@ -803,11 +879,10 @@ const __VLS_self = (await import('vue')).defineComponent({
|
|||
ScrollAreaScrollbar: ScrollAreaScrollbar,
|
||||
ScrollAreaThumb: ScrollAreaThumb,
|
||||
MessageListItem: MessageListItem,
|
||||
Input: Input,
|
||||
SearchBar: SearchBar,
|
||||
Dialog: Dialog,
|
||||
Button: Button,
|
||||
store: store,
|
||||
searchValue: searchValue,
|
||||
showTrashConfirm: showTrashConfirm,
|
||||
folderColor: folderColor,
|
||||
displayMessages: displayMessages,
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -5,6 +5,7 @@ import { Paperclip, Star } from 'lucide-vue-next'
|
|||
import { TooltipRoot, TooltipTrigger, TooltipContent, TooltipPortal } from 'radix-vue'
|
||||
import { cn } from '../../lib/utils'
|
||||
import Badge from '../ui/Badge.vue'
|
||||
import { useMailStore } from '../../stores/mail'
|
||||
|
||||
const props = defineProps({
|
||||
message: { type: Object, required: true },
|
||||
|
|
@ -12,6 +13,8 @@ const props = defineProps({
|
|||
folderColor: { type: String, default: '' },
|
||||
})
|
||||
|
||||
const store = useMailStore()
|
||||
|
||||
const timestamp = computed(() => props.message.received_at || props.message.sent_at)
|
||||
const relativeTime = computed(() =>
|
||||
timestamp.value ? formatDistanceToNow(new Date(timestamp.value), { addSuffix: true }) : ''
|
||||
|
|
@ -19,6 +22,22 @@ const relativeTime = computed(() =>
|
|||
const exactTime = computed(() =>
|
||||
timestamp.value ? format(new Date(timestamp.value), 'PPpp') : ''
|
||||
)
|
||||
|
||||
const isSentOrDrafts = computed(() => {
|
||||
const name = store.currentFolderObj?.name?.toLowerCase() ?? ''
|
||||
return name === 'sent' || name === 'drafts'
|
||||
})
|
||||
|
||||
const recipientLabel = computed(() => {
|
||||
if (!isSentOrDrafts.value) return null
|
||||
let addrs: string[] = []
|
||||
try { addrs = JSON.parse(props.message.to_addresses || '[]') } catch { addrs = [] }
|
||||
if (!addrs.length) return null
|
||||
return 'To: ' + addrs.map((a: string) => {
|
||||
const m = a.match(/<([^>]+)>/)
|
||||
return m ? m[1] : a
|
||||
}).join(', ')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -32,7 +51,7 @@ const exactTime = computed(() =>
|
|||
<div class="flex w-full flex-col gap-1">
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="font-semibold">{{ message.from_name || message.from_address }}</div>
|
||||
<div class="font-semibold">{{ recipientLabel ?? (message.from_name || message.from_address) }}</div>
|
||||
<span v-if="!message.is_read" class="flex h-2 w-2 rounded-full bg-primary" />
|
||||
<Star v-if="message.is_starred" class="size-3 fill-yellow-400 text-yellow-400" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,14 +5,37 @@ import { Paperclip, Star } from 'lucide-vue-next';
|
|||
import { TooltipRoot, TooltipTrigger, TooltipContent, TooltipPortal } from 'radix-vue';
|
||||
import { cn } from '../../lib/utils';
|
||||
import Badge from '../ui/Badge.vue';
|
||||
import { useMailStore } from '../../stores/mail';
|
||||
const props = defineProps({
|
||||
message: { type: Object, required: true },
|
||||
selected: { type: Boolean, default: false },
|
||||
folderColor: { type: String, default: '' },
|
||||
});
|
||||
const store = useMailStore();
|
||||
const timestamp = computed(() => props.message.received_at || props.message.sent_at);
|
||||
const relativeTime = computed(() => timestamp.value ? formatDistanceToNow(new Date(timestamp.value), { addSuffix: true }) : '');
|
||||
const exactTime = computed(() => timestamp.value ? format(new Date(timestamp.value), 'PPpp') : '');
|
||||
const isSentOrDrafts = computed(() => {
|
||||
const name = store.currentFolderObj?.name?.toLowerCase() ?? '';
|
||||
return name === 'sent' || name === 'drafts';
|
||||
});
|
||||
const recipientLabel = computed(() => {
|
||||
if (!isSentOrDrafts.value)
|
||||
return null;
|
||||
let addrs = [];
|
||||
try {
|
||||
addrs = JSON.parse(props.message.to_addresses || '[]');
|
||||
}
|
||||
catch {
|
||||
addrs = [];
|
||||
}
|
||||
if (!addrs.length)
|
||||
return null;
|
||||
return 'To: ' + addrs.map((a) => {
|
||||
const m = a.match(/<([^>]+)>/);
|
||||
return m ? m[1] : a;
|
||||
}).join(', ');
|
||||
});
|
||||
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
|
||||
const __VLS_ctx = {};
|
||||
let __VLS_components;
|
||||
|
|
@ -33,7 +56,7 @@ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.d
|
|||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "font-semibold" },
|
||||
});
|
||||
(__VLS_ctx.message.from_name || __VLS_ctx.message.from_address);
|
||||
(__VLS_ctx.recipientLabel ?? (__VLS_ctx.message.from_name || __VLS_ctx.message.from_address));
|
||||
if (!__VLS_ctx.message.is_read) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span)({
|
||||
...{ class: "flex h-2 w-2 rounded-full bg-primary" },
|
||||
|
|
@ -183,6 +206,7 @@ const __VLS_self = (await import('vue')).defineComponent({
|
|||
timestamp: timestamp,
|
||||
relativeTime: relativeTime,
|
||||
exactTime: exactTime,
|
||||
recipientLabel: recipientLabel,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -14,7 +14,7 @@ const store = useMailStore()
|
|||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||
@click.self="store.isSettingsOpen = false"
|
||||
>
|
||||
<div class="relative w-full max-w-3xl rounded-xl border bg-background shadow-xl mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div class="relative w-full max-w-5xl rounded-xl border bg-background shadow-xl mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<button
|
||||
class="absolute right-4 top-4 inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
|
||||
@click="store.isSettingsOpen = false"
|
||||
|
|
|
|||
115
webmail/src/components/mail/SettingsDialog.vue.js
Normal file
115
webmail/src/components/mail/SettingsDialog.vue.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/// <reference types="../../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
|
||||
import { X } from 'lucide-vue-next';
|
||||
import { useMailStore } from '../../stores/mail';
|
||||
import SettingsContent from '../settings/SettingsContent.vue';
|
||||
const store = useMailStore();
|
||||
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
|
||||
const __VLS_ctx = {};
|
||||
let __VLS_components;
|
||||
let __VLS_directives;
|
||||
// CSS variable injection
|
||||
// CSS variable injection end
|
||||
const __VLS_0 = {}.Teleport;
|
||||
/** @type {[typeof __VLS_components.Teleport, typeof __VLS_components.Teleport, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_1 = __VLS_asFunctionalComponent(__VLS_0, new __VLS_0({
|
||||
to: "body",
|
||||
}));
|
||||
const __VLS_2 = __VLS_1({
|
||||
to: "body",
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
|
||||
__VLS_3.slots.default;
|
||||
const __VLS_4 = {}.Transition;
|
||||
/** @type {[typeof __VLS_components.Transition, typeof __VLS_components.Transition, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_5 = __VLS_asFunctionalComponent(__VLS_4, new __VLS_4({
|
||||
name: "fade",
|
||||
}));
|
||||
const __VLS_6 = __VLS_5({
|
||||
name: "fade",
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_5));
|
||||
__VLS_7.slots.default;
|
||||
if (__VLS_ctx.store.isSettingsOpen) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ onClick: (...[$event]) => {
|
||||
if (!(__VLS_ctx.store.isSettingsOpen))
|
||||
return;
|
||||
__VLS_ctx.store.isSettingsOpen = false;
|
||||
} },
|
||||
...{ class: "fixed inset-0 z-50 flex items-center justify-center bg-black/50" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "relative w-full max-w-5xl rounded-xl border bg-background shadow-xl mx-4 max-h-[90vh] overflow-y-auto" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
|
||||
...{ onClick: (...[$event]) => {
|
||||
if (!(__VLS_ctx.store.isSettingsOpen))
|
||||
return;
|
||||
__VLS_ctx.store.isSettingsOpen = false;
|
||||
} },
|
||||
...{ class: "absolute right-4 top-4 inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors" },
|
||||
});
|
||||
const __VLS_8 = {}.X;
|
||||
/** @type {[typeof __VLS_components.X, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_9 = __VLS_asFunctionalComponent(__VLS_8, new __VLS_8({
|
||||
...{ class: "size-4" },
|
||||
}));
|
||||
const __VLS_10 = __VLS_9({
|
||||
...{ class: "size-4" },
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_9));
|
||||
/** @type {[typeof SettingsContent, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_12 = __VLS_asFunctionalComponent(SettingsContent, new SettingsContent({}));
|
||||
const __VLS_13 = __VLS_12({}, ...__VLS_functionalComponentArgsRest(__VLS_12));
|
||||
}
|
||||
var __VLS_7;
|
||||
var __VLS_3;
|
||||
/** @type {__VLS_StyleScopedClasses['fixed']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['inset-0']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['z-50']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['bg-black/50']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['relative']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['max-w-5xl']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['border']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['bg-background']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['shadow-xl']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['mx-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['max-h-[90vh]']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['overflow-y-auto']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['absolute']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['right-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['top-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['h-7']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['w-7']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['hover:bg-accent']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['hover:text-accent-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['size-4']} */ ;
|
||||
var __VLS_dollars;
|
||||
const __VLS_self = (await import('vue')).defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
X: X,
|
||||
SettingsContent: SettingsContent,
|
||||
store: store,
|
||||
};
|
||||
},
|
||||
});
|
||||
export default (await import('vue')).defineComponent({
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
; /* PartiallyEnd: #4569/main.vue */
|
||||
//# sourceMappingURL=SettingsDialog.vue.js.map
|
||||
1
webmail/src/components/mail/SettingsDialog.vue.js.map
Normal file
1
webmail/src/components/mail/SettingsDialog.vue.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"SettingsDialog.vue.js","sourceRoot":"","sources":["SettingsDialog.vue"],"names":[],"mappings":"AAwCA,oFAAoF;AAEpF,OAAO,EAAE,CAAC,EAAE,MAAM,iBAAiB,CAAA;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,eAAe,MAAM,iCAAiC,CAAA;AAE7D,MAAM,KAAK,GAAG,YAAY,EAAE,CAAA;AAC5B,QAAQ,CAAA,CAAA,yCAAyC;AAIjD,MAAM,SAAS,GAAG,EAAqE,CAAC;AAExF,IAAI,gBAAiE,CAAC;AAEtE,IAAI,gBAAiE,CAAC;AAMtE,0BAA0B;AAC1B,8BAA8B;AAC9B,MAAM,OAAO,GAAI,EAAuG,CAAC,QAAQ,CAAC;AAClI,qFAAqF,CAAA,CAAC;AACtF,aAAa;AACb,MAAM,OAAO,GAAG,2BAA2B,CAAC,OAAO,EAAE,IAAI,OAAO,CAAC;IACjE,EAAE,EAAE,MAAM;CACT,CAAC,CAAC,CAAC;AACJ,MAAM,OAAO,GAAG,OAAO,CAAC;IACxB,EAAE,EAAE,MAAM;CACT,EAAE,GAAG,iCAAiC,CAAC,OAAO,CAAC,CAAC,CAAC;AAClD,OAAO,CAAC,KAAM,CAAC,OAAO,CAAC;AACvB,MAAM,OAAO,GAAI,EAA+G,CAAC,UAAU,CAAC;AAC5I,yFAAyF,CAAA,CAAC;AAC1F,aAAa;AACb,MAAM,OAAO,GAAG,2BAA2B,CAAC,OAAO,EAAE,IAAI,OAAO,CAAC;IACjE,IAAI,EAAE,MAAM;CACX,CAAC,CAAC,CAAC;AACJ,MAAM,OAAO,GAAG,OAAO,CAAC;IACxB,IAAI,EAAE,MAAM;CACX,EAAE,GAAG,iCAAiC,CAAC,OAAO,CAAC,CAAC,CAAC;AAClD,OAAO,CAAC,KAAM,CAAC,OAAO,CAAC;AACvB,IAAI,SAAS,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC;IACrC,yBAAyB,CAAC,uBAAuB,CAAC,GAAG,EAAE,uBAAuB,CAAC,GAAG,CAAC,CAAC;QACpF,GAAG,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE;gBAC9B,IAAI,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,cAAc,CAAC;oBAAE,OAAO;gBAC9C,SAAS,CAAC,KAAK,CAAC,cAAc,GAAG,KAAK,CAAC;YACvC,CAAC,EAAC;QACF,GAAG,EAAE,KAAK,EAAE,iEAAiE,EAAE;KAC9E,CAAC,CAAC;IACH,yBAAyB,CAAC,uBAAuB,CAAC,GAAG,EAAE,uBAAuB,CAAC,GAAG,CAAC,CAAC;QACpF,GAAG,EAAE,KAAK,EAAE,uGAAuG,EAAE;KACpH,CAAC,CAAC;IACH,yBAAyB,CAAC,uBAAuB,CAAC,MAAM,EAAE,uBAAuB,CAAC,MAAM,CAAC,CAAC;QAC1F,GAAG,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE;gBAC9B,IAAI,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,cAAc,CAAC;oBAAE,OAAO;gBAC9C,SAAS,CAAC,KAAK,CAAC,cAAc,GAAG,KAAK,CAAC;YACvC,CAAC,EAAC;QACF,GAAG,EAAE,KAAK,EAAE,wKAAwK,EAAE;KACrL,CAAC,CAAC;IACH,MAAM,OAAO,GAAI,EAA2E,CAAC,CAAC,CAAC;IAC/F,4CAA4C,CAAA,CAAC;IAC7C,aAAa;IACb,MAAM,OAAO,GAAG,2BAA2B,CAAC,OAAO,EAAE,IAAI,OAAO,CAAC;QACjE,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE;KACrB,CAAC,CAAC,CAAC;IACJ,MAAM,QAAQ,GAAG,OAAO,CAAC;QACzB,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE;KACrB,EAAE,GAAG,iCAAiC,CAAC,OAAO,CAAC,CAAC,CAAC;IAClD,yCAAyC,CAAA,CAAC;IAC1C,aAAa;IACb,MAAM,QAAQ,GAAG,2BAA2B,CAAC,eAAe,EAAE,IAAI,eAAe,CAAC,EACjF,CAAC,CAAC,CAAC;IACJ,MAAM,QAAQ,GAAG,QAAQ,CAAC,EACzB,EAAE,GAAG,iCAAiC,CAAC,QAAQ,CAAC,CAAC,CAAC;AACnD,CAAC;AACD,IAAI,OAA0E,CAAC;AAC/E,IAAI,OAA0E,CAAC;AAC/E,gDAAgD,CAAA,CAAC;AACjD,kDAAkD,CAAA,CAAC;AACnD,+CAA+C,CAAA,CAAC;AAChD,+CAA+C,CAAA,CAAC;AAChD,uDAAuD,CAAA,CAAC;AACxD,yDAAyD,CAAA,CAAC;AAC1D,sDAAsD,CAAA,CAAC;AACvD,mDAAmD,CAAA,CAAC;AACpD,iDAAiD,CAAA,CAAC;AAClD,oDAAoD,CAAA,CAAC;AACrD,qDAAqD,CAAA,CAAC;AACtD,iDAAiD,CAAA,CAAC;AAClD,wDAAwD,CAAA,CAAC;AACzD,oDAAoD,CAAA,CAAC;AACrD,+CAA+C,CAAA,CAAC;AAChD,uDAAuD,CAAA,CAAC;AACxD,0DAA0D,CAAA,CAAC;AAC3D,mDAAmD,CAAA,CAAC;AACpD,kDAAkD,CAAA,CAAC;AACnD,gDAAgD,CAAA,CAAC;AACjD,sDAAsD,CAAA,CAAC;AACvD,8CAA8C,CAAA,CAAC;AAC/C,8CAA8C,CAAA,CAAC;AAC/C,uDAAuD,CAAA,CAAC;AACxD,yDAAyD,CAAA,CAAC;AAC1D,qDAAqD,CAAA,CAAC;AACtD,gEAAgE,CAAA,CAAC;AACjE,0DAA0D,CAAA,CAAC;AAC3D,uEAAuE,CAAA,CAAC;AACxE,4DAA4D,CAAA,CAAC;AAC7D,iDAAiD,CAAA,CAAC;AAOlD,IAAI,aAK+D,CAAC;AACpE,MAAM,UAAU,GAAG,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,eAAe,CAAC;IACzD,KAAK;QACL,OAAO;YACP,CAAC,EAAE,CAAa;YAChB,eAAe,EAAE,eAAyC;YAC1D,KAAK,EAAE,KAAqB;SAC3B,CAAC;IACF,CAAC;CACA,CAAC,CAAC;AACH,eAAe,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,eAAe,CAAC;IACrD,KAAK;QACL,OAAO,EACN,CAAC;IACF,CAAC;CACA,CAAC,CAAC;AACH,CAAC,CAAA,kCAAkC"}
|
||||
|
|
@ -6,6 +6,7 @@ import { usePushSubscription } from '@/composables/usePushSubscription'
|
|||
import { useMailStore } from '@/stores/mail'
|
||||
import { mailApi } from '@/api/mail'
|
||||
import { authApi } from '@/api/auth'
|
||||
import type { Alias } from '@/types/mail'
|
||||
|
||||
const { canInstall, promptInstall } = useInstallPrompt()
|
||||
const notificationsStore = useNotificationsStore()
|
||||
|
|
@ -167,6 +168,98 @@ async function deleteAutoResponder() {
|
|||
arDeleteLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const alAliases = ref<Alias[]>([])
|
||||
const alLoading = ref(false)
|
||||
const alError = ref('')
|
||||
const alSuccess = ref('')
|
||||
const alNewAddress = ref('')
|
||||
const alNewLabel = ref('')
|
||||
const alNewDescription = ref('')
|
||||
const alNewExpires = ref('')
|
||||
const alNeverExpire = ref(true)
|
||||
const alCreateLoading = ref(false)
|
||||
const alGenerating = ref(false)
|
||||
|
||||
async function loadAliases(address: string) {
|
||||
alLoading.value = true
|
||||
alError.value = ''
|
||||
try {
|
||||
const res = await mailApi.getAllAliases(address)
|
||||
alAliases.value = res.data.aliases || []
|
||||
} catch {
|
||||
alError.value = 'Failed to load aliases.'
|
||||
} finally {
|
||||
alLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => mailStore.currentMailbox,
|
||||
(address) => { if (address) loadAliases(address) },
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function generateAlias() {
|
||||
if (!mailStore.currentMailbox) return
|
||||
const domain = mailStore.currentMailbox.split('@')[1]
|
||||
if (!domain) return
|
||||
alGenerating.value = true
|
||||
try {
|
||||
const res = await mailApi.generateAlias(mailStore.currentMailbox, domain)
|
||||
alNewAddress.value = res.data.suggestion?.split('@')[0] || ''
|
||||
} catch { /* ignore */ } finally {
|
||||
alGenerating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createAlias() {
|
||||
if (!mailStore.currentMailbox || !alNewAddress.value) return
|
||||
const domain = mailStore.currentMailbox.split('@')[1]
|
||||
alError.value = ''
|
||||
alSuccess.value = ''
|
||||
alCreateLoading.value = true
|
||||
try {
|
||||
await mailApi.createAlias({
|
||||
address: `${alNewAddress.value}@${domain}`,
|
||||
mailbox_address: mailStore.currentMailbox,
|
||||
label: alNewLabel.value || undefined,
|
||||
description: alNewDescription.value || undefined,
|
||||
expires_at: alNeverExpire.value ? null : (alNewExpires.value || null),
|
||||
})
|
||||
alNewAddress.value = ''
|
||||
alNewLabel.value = ''
|
||||
alNewDescription.value = ''
|
||||
alNewExpires.value = ''
|
||||
alNeverExpire.value = true
|
||||
alSuccess.value = 'Alias created.'
|
||||
await loadAliases(mailStore.currentMailbox)
|
||||
} catch (e: any) {
|
||||
alError.value = e?.response?.data?.error || 'Failed to create alias.'
|
||||
} finally {
|
||||
alCreateLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleAlias(alias: Alias) {
|
||||
try {
|
||||
await mailApi.updateAlias(alias.address, { is_active: !alias.is_active })
|
||||
alias.is_active = !alias.is_active
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
async function deleteAlias(alias: Alias) {
|
||||
if (!confirm(`Delete alias ${alias.address}? This cannot be undone.`)) return
|
||||
try {
|
||||
await mailApi.deleteAlias(alias.address)
|
||||
alAliases.value = alAliases.value.filter(a => a.address !== alias.address)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function formatDate(iso: string | null) {
|
||||
if (!iso) return '—'
|
||||
return new Date(iso).toLocaleDateString()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -410,6 +503,111 @@ async function deleteAutoResponder() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="mailStore.currentMailbox" class="mb-4 rounded-lg border p-4 space-y-3">
|
||||
<div>
|
||||
<h2 class="text-sm font-medium">Email Aliases</h2>
|
||||
<p class="text-sm text-muted-foreground mt-0.5">
|
||||
Create aliases that forward mail to your mailbox while keeping your real address private.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="alLoading" class="text-sm text-muted-foreground">Loading…</div>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="alAliases.length" class="divide-y divide-border rounded-md border overflow-hidden">
|
||||
<div
|
||||
v-for="alias in alAliases"
|
||||
:key="alias.address"
|
||||
class="flex items-center gap-3 px-3 py-2.5 bg-background text-sm"
|
||||
>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-mono text-xs truncate">{{ alias.address }}</div>
|
||||
<div class="flex items-center gap-2 mt-0.5">
|
||||
<span v-if="alias.label" class="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">{{ alias.label }}</span>
|
||||
<span v-if="alias.expires_at" class="text-xs text-muted-foreground">Expires {{ formatDate(alias.expires_at) }}</span>
|
||||
<span class="text-xs text-muted-foreground">{{ alias.use_count }} received</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200"
|
||||
:class="alias.is_active ? 'bg-primary' : 'bg-muted'"
|
||||
role="switch"
|
||||
:aria-checked="alias.is_active"
|
||||
@click="toggleAlias(alias)"
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow ring-0 transition-transform duration-200"
|
||||
:class="alias.is_active ? 'translate-x-4' : 'translate-x-0'"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="text-muted-foreground hover:text-destructive transition-colors text-xs"
|
||||
@click="deleteAlias(alias)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-sm text-muted-foreground">No aliases yet.</p>
|
||||
|
||||
<div class="space-y-2 pt-1">
|
||||
<label class="text-sm font-medium">Create alias</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="alNewAddress"
|
||||
type="text"
|
||||
placeholder="local-part"
|
||||
class="flex-1 rounded-md border bg-background px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<span class="text-sm text-muted-foreground shrink-0">@{{ mailStore.currentMailbox.split('@')[1] }}</span>
|
||||
<button
|
||||
:disabled="alGenerating"
|
||||
class="inline-flex items-center justify-center rounded-md border px-3 py-2 text-sm font-medium hover:bg-accent transition-colors disabled:opacity-50 shrink-0"
|
||||
@click="generateAlias"
|
||||
>
|
||||
{{ alGenerating ? '…' : 'Generate' }}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
v-model="alNewLabel"
|
||||
type="text"
|
||||
placeholder="Label (optional, e.g. shopping)"
|
||||
class="w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<input
|
||||
v-model="alNewDescription"
|
||||
type="text"
|
||||
placeholder="Description (optional)"
|
||||
maxlength="200"
|
||||
class="w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input v-model="alNeverExpire" type="checkbox" class="rounded border-border" />
|
||||
Never expire
|
||||
</label>
|
||||
<input
|
||||
v-if="!alNeverExpire"
|
||||
v-model="alNewExpires"
|
||||
type="date"
|
||||
class="rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="alError" class="text-xs text-destructive">{{ alError }}</p>
|
||||
<p v-if="alSuccess" class="text-xs text-green-600">{{ alSuccess }}</p>
|
||||
|
||||
<button
|
||||
:disabled="alCreateLoading || !alNewAddress"
|
||||
class="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50"
|
||||
@click="createAlias"
|
||||
>
|
||||
{{ alCreateLoading ? 'Creating…' : 'Create Alias' }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-muted-foreground">Additional settings are managed in DockFlare Master.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
1308
webmail/src/components/settings/SettingsContent.vue.js
Normal file
1308
webmail/src/components/settings/SettingsContent.vue.js
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
15
webmail/src/composables/useBreakpoint.js
Normal file
15
webmail/src/composables/useBreakpoint.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
const isMobile = ref(false);
|
||||
export function useBreakpoint() {
|
||||
const mq = window.matchMedia('(max-width: 767px)');
|
||||
const update = (e) => {
|
||||
isMobile.value = e.matches;
|
||||
};
|
||||
onMounted(() => {
|
||||
isMobile.value = mq.matches;
|
||||
mq.addEventListener('change', update);
|
||||
});
|
||||
onUnmounted(() => mq.removeEventListener('change', update));
|
||||
return { isMobile };
|
||||
}
|
||||
//# sourceMappingURL=useBreakpoint.js.map
|
||||
1
webmail/src/composables/useBreakpoint.js.map
Normal file
1
webmail/src/composables/useBreakpoint.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"useBreakpoint.js","sourceRoot":"","sources":["useBreakpoint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,KAAK,CAAA;AAEjD,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;AAE3B,MAAM,UAAU,aAAa;IAC3B,MAAM,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC,oBAAoB,CAAC,CAAA;IAElD,MAAM,MAAM,GAAG,CAAC,CAAuC,EAAE,EAAE;QACzD,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAC,OAAO,CAAA;IAC5B,CAAC,CAAA;IAED,SAAS,CAAC,GAAG,EAAE;QACb,QAAQ,CAAC,KAAK,GAAG,EAAE,CAAC,OAAO,CAAA;QAC3B,EAAE,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;IACvC,CAAC,CAAC,CAAA;IAEF,WAAW,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAA;IAE3D,OAAO,EAAE,QAAQ,EAAE,CAAA;AACrB,CAAC"}
|
||||
20
webmail/src/composables/useBreakpoint.ts
Normal file
20
webmail/src/composables/useBreakpoint.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const isMobile = ref(false)
|
||||
|
||||
export function useBreakpoint() {
|
||||
const mq = window.matchMedia('(max-width: 767px)')
|
||||
|
||||
const update = (e: MediaQueryListEvent | MediaQueryList) => {
|
||||
isMobile.value = e.matches
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
isMobile.value = mq.matches
|
||||
mq.addEventListener('change', update)
|
||||
})
|
||||
|
||||
onUnmounted(() => mq.removeEventListener('change', update))
|
||||
|
||||
return { isMobile }
|
||||
}
|
||||
|
|
@ -1,18 +1,41 @@
|
|||
import { onUnmounted, ref, watch } from 'vue';
|
||||
import { mailApi } from '@/api/mail';
|
||||
import { useNotificationsStore } from '@/stores/notifications';
|
||||
import { useMailStore } from '@/stores/mail';
|
||||
function updateBadge(count) {
|
||||
if (!('setAppBadge' in navigator))
|
||||
return;
|
||||
if (count > 0) {
|
||||
navigator.setAppBadge(count).catch(() => { });
|
||||
}
|
||||
else {
|
||||
navigator.clearAppBadge().catch(() => { });
|
||||
}
|
||||
}
|
||||
async function getPushIntervalMs() {
|
||||
try {
|
||||
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||||
const reg = await navigator.serviceWorker.ready;
|
||||
const sub = await reg.pushManager.getSubscription();
|
||||
if (sub)
|
||||
return 300_000;
|
||||
}
|
||||
}
|
||||
catch { /* SW not available */ }
|
||||
return 30_000 + Math.random() * 20_000 - 10_000;
|
||||
}
|
||||
export function useMailPolling() {
|
||||
const notificationsStore = useNotificationsStore();
|
||||
const mailStore = useMailStore();
|
||||
const lastSeen = ref({});
|
||||
const initialized = ref(false);
|
||||
let intervalId = null;
|
||||
const poll = async () => {
|
||||
if (mailStore.mailboxes.length === 0)
|
||||
return;
|
||||
try {
|
||||
const res = await mailApi.getMailboxStatus();
|
||||
const statuses = res.data;
|
||||
const totalUnread = statuses.reduce((sum, s) => sum + s.unread_count, 0);
|
||||
updateBadge(totalUnread);
|
||||
if (!initialized.value) {
|
||||
for (const s of statuses) {
|
||||
lastSeen.value[s.address] = s.latest_received_at;
|
||||
|
|
@ -20,14 +43,36 @@ export function useMailPolling() {
|
|||
initialized.value = true;
|
||||
return;
|
||||
}
|
||||
if (!notificationsStore.isGranted)
|
||||
return;
|
||||
for (const s of statuses) {
|
||||
const prev = lastSeen.value[s.address];
|
||||
if (s.latest_received_at &&
|
||||
(prev === undefined || prev === null || s.latest_received_at > prev)) {
|
||||
lastSeen.value[s.address] = s.latest_received_at;
|
||||
fireNotification(s.address, s.unread_count);
|
||||
if (s.address === mailStore.currentMailbox && mailStore.currentFolder) {
|
||||
try {
|
||||
const mRes = await mailApi.getMessages(s.address, {
|
||||
folder: mailStore.currentFolder,
|
||||
order: mailStore.sortOrder,
|
||||
page: 1,
|
||||
per_page: 50,
|
||||
});
|
||||
const payload = mRes.data;
|
||||
const items = Array.isArray(payload) ? payload : payload.items || [];
|
||||
mailStore.messages = items;
|
||||
mailStore.totalMessages = payload.total ?? items.length;
|
||||
mailStore.messagesPage = 1;
|
||||
mailStore.hasMoreMessages = items.length === 50;
|
||||
}
|
||||
catch { /* network error — skip */ }
|
||||
try {
|
||||
const fRes = await mailApi.getFolders(s.address);
|
||||
mailStore.folders = fRes.data;
|
||||
}
|
||||
catch { /* network error — skip */ }
|
||||
}
|
||||
if (Notification.permission === 'granted') {
|
||||
fireNotification(s.address, s.unread_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -48,11 +93,34 @@ export function useMailPolling() {
|
|||
n.close();
|
||||
};
|
||||
};
|
||||
const startInterval = async () => {
|
||||
if (intervalId)
|
||||
clearInterval(intervalId);
|
||||
const ms = await getPushIntervalMs();
|
||||
intervalId = setInterval(poll, ms);
|
||||
};
|
||||
const onVisibilityChange = () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
poll();
|
||||
startInterval();
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
watch(() => mailStore.mailboxes, (boxes) => {
|
||||
if (boxes.length > 0 && !initialized.value)
|
||||
poll();
|
||||
}, { immediate: true });
|
||||
const interval = setInterval(poll, 60_000);
|
||||
onUnmounted(() => clearInterval(interval));
|
||||
startInterval();
|
||||
onUnmounted(() => {
|
||||
if (intervalId)
|
||||
clearInterval(intervalId);
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=useMailPolling.js.map
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"useMailPolling.js","sourceRoot":"","sources":["useMailPolling.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"useMailPolling.js","sourceRoot":"","sources":["useMailPolling.ts"],"names":[],"mappings":"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"}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { ref } from 'vue';
|
||||
import apiClient from '@/api/client';
|
||||
import { useMailStore } from '@/stores/mail';
|
||||
const isSupported = typeof window !== 'undefined' && 'serviceWorker' in navigator && 'PushManager' in window;
|
||||
function urlBase64ToUint8Array(base64) {
|
||||
const padding = '='.repeat((4 - (base64.length % 4)) % 4);
|
||||
|
|
@ -12,6 +13,8 @@ function urlBase64ToUint8Array(base64) {
|
|||
export function usePushSubscription() {
|
||||
const isSubscribed = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const error = ref(null);
|
||||
const mailStore = useMailStore();
|
||||
const checkSubscription = async () => {
|
||||
if (!isSupported)
|
||||
return;
|
||||
|
|
@ -19,10 +22,11 @@ export function usePushSubscription() {
|
|||
const sub = await reg.pushManager.getSubscription();
|
||||
isSubscribed.value = !!sub;
|
||||
};
|
||||
const subscribe = async (mailboxAddress) => {
|
||||
const subscribe = async () => {
|
||||
if (!isSupported)
|
||||
return;
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const { data } = await apiClient.get('/notifications/vapid-key');
|
||||
const reg = await navigator.serviceWorker.ready;
|
||||
|
|
@ -31,13 +35,28 @@ export function usePushSubscription() {
|
|||
applicationServerKey: urlBase64ToUint8Array(data.public_key),
|
||||
});
|
||||
const subJson = sub.toJSON();
|
||||
await apiClient.post('/notifications/subscribe', {
|
||||
endpoint: subJson.endpoint,
|
||||
keys: subJson.keys,
|
||||
mailbox_address: mailboxAddress,
|
||||
});
|
||||
let addresses = mailStore.mailboxes.map((m) => m.address);
|
||||
if (addresses.length === 0) {
|
||||
const statusRes = await apiClient.get('/mailboxes/status');
|
||||
addresses = statusRes.data.map((s) => s.address);
|
||||
}
|
||||
if (addresses.length === 0) {
|
||||
error.value = 'No mailboxes found';
|
||||
return;
|
||||
}
|
||||
for (const address of addresses) {
|
||||
await apiClient.post('/notifications/subscribe', {
|
||||
endpoint: subJson.endpoint,
|
||||
keys: subJson.keys,
|
||||
mailbox_address: address,
|
||||
});
|
||||
}
|
||||
isSubscribed.value = true;
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Push subscribe failed:', err);
|
||||
error.value = err?.message ?? 'Subscription failed';
|
||||
}
|
||||
finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
|
@ -46,6 +65,7 @@ export function usePushSubscription() {
|
|||
if (!isSupported)
|
||||
return;
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const reg = await navigator.serviceWorker.ready;
|
||||
const sub = await reg.pushManager.getSubscription();
|
||||
|
|
@ -57,11 +77,15 @@ export function usePushSubscription() {
|
|||
}
|
||||
isSubscribed.value = false;
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Push unsubscribe failed:', err);
|
||||
error.value = err?.message ?? 'Unsubscribe failed';
|
||||
}
|
||||
finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
checkSubscription();
|
||||
return { isSubscribed, isLoading, isSupported, subscribe, unsubscribe };
|
||||
return { isSubscribed, isLoading, isSupported, error, subscribe, unsubscribe };
|
||||
}
|
||||
//# sourceMappingURL=usePushSubscription.js.map
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"usePushSubscription.js","sourceRoot":"","sources":["usePushSubscription.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"usePushSubscription.js","sourceRoot":"","sources":["usePushSubscription.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AACzB,OAAO,SAAS,MAAM,cAAc,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAE5C,MAAM,WAAW,GAAG,OAAO,MAAM,KAAK,WAAW,IAAI,eAAe,IAAI,SAAS,IAAI,aAAa,IAAI,MAAM,CAAA;AAE5G,SAAS,qBAAqB,CAAC,MAAc;IAC3C,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IACzD,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,MAAM,GAAG,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAA;IAC1E,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IACxC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,KAAK,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;IACjE,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,MAAM,YAAY,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IAC/B,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IAC5B,MAAM,KAAK,GAAG,GAAG,CAAgB,IAAI,CAAC,CAAA;IACtC,MAAM,SAAS,GAAG,YAAY,EAAE,CAAA;IAEhC,MAAM,iBAAiB,GAAG,KAAK,IAAI,EAAE;QACnC,IAAI,CAAC,WAAW;YAAE,OAAM;QACxB,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,aAAa,CAAC,KAAK,CAAA;QAC/C,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,WAAW,CAAC,eAAe,EAAE,CAAA;QACnD,YAAY,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAA;IAC5B,CAAC,CAAA;IAED,MAAM,SAAS,GAAG,KAAK,IAAI,EAAE;QAC3B,IAAI,CAAC,WAAW;YAAE,OAAM;QACxB,SAAS,CAAC,KAAK,GAAG,IAAI,CAAA;QACtB,KAAK,CAAC,KAAK,GAAG,IAAI,CAAA;QAClB,IAAI,CAAC;YACH,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAA;YAEhE,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,aAAa,CAAC,KAAK,CAAA;YAC/C,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,WAAW,CAAC,SAAS,CAAC;gBAC1C,eAAe,EAAE,IAAI;gBACrB,oBAAoB,EAAE,qBAAqB,CAAC,IAAI,CAAC,UAAU,CAAC;aAC7D,CAAC,CAAA;YACF,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE,CAAA;YAE5B,IAAI,SAAS,GAAa,SAAS,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;YACxE,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC3B,MAAM,SAAS,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAA;gBAC1D,SAAS,GAAI,SAAS,CAAC,IAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;YAC7D,CAAC;YAED,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC3B,KAAK,CAAC,KAAK,GAAG,oBAAoB,CAAA;gBAClC,OAAM;YACR,CAAC;YAED,KAAK,MAAM,OAAO,IAAI,SAAS,EAAE,CAAC;gBAChC,MAAM,SAAS,CAAC,IAAI,CAAC,0BAA0B,EAAE;oBAC/C,QAAQ,EAAE,OAAO,CAAC,QAAQ;oBAC1B,IAAI,EAAE,OAAO,CAAC,IAAI;oBAClB,eAAe,EAAE,OAAO;iBACzB,CAAC,CAAA;YACJ,CAAC;YACD,YAAY,CAAC,KAAK,GAAG,IAAI,CAAA;QAC3B,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,GAAG,CAAC,CAAA;YAC5C,KAAK,CAAC,KAAK,GAAG,GAAG,EAAE,OAAO,IAAI,qBAAqB,CAAA;QACrD,CAAC;gBAAS,CAAC;YACT,SAAS,CAAC,KAAK,GAAG,KAAK,CAAA;QACzB,CAAC;IACH,CAAC,CAAA;IAED,MAAM,WAAW,GAAG,KAAK,IAAI,EAAE;QAC7B,IAAI,CAAC,WAAW;YAAE,OAAM;QACxB,SAAS,CAAC,KAAK,GAAG,IAAI,CAAA;QACtB,KAAK,CAAC,KAAK,GAAG,IAAI,CAAA;QAClB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,aAAa,CAAC,KAAK,CAAA;YAC/C,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,WAAW,CAAC,eAAe,EAAE,CAAA;YACnD,IAAI,GAAG,EAAE,CAAC;gBACR,MAAM,SAAS,CAAC,MAAM,CAAC,0BAA0B,EAAE;oBACjD,IAAI,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE;iBACjC,CAAC,CAAA;gBACF,MAAM,GAAG,CAAC,WAAW,EAAE,CAAA;YACzB,CAAC;YACD,YAAY,CAAC,KAAK,GAAG,KAAK,CAAA;QAC5B,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,OAAO,CAAC,KAAK,CAAC,0BAA0B,EAAE,GAAG,CAAC,CAAA;YAC9C,KAAK,CAAC,KAAK,GAAG,GAAG,EAAE,OAAO,IAAI,oBAAoB,CAAA;QACpD,CAAC;gBAAS,CAAC;YACT,SAAS,CAAC,KAAK,GAAG,KAAK,CAAA;QACzB,CAAC;IACH,CAAC,CAAA;IAED,iBAAiB,EAAE,CAAA;IAEnB,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,CAAA;AAChF,CAAC"}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
import { setIDBItem, removeIDBItem } from '@/lib/idb';
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const token = ref(localStorage.getItem('jwt_token') || '');
|
||||
const isAuthenticated = computed(() => {
|
||||
|
|
@ -16,6 +17,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
const setToken = (newToken) => {
|
||||
token.value = newToken;
|
||||
localStorage.setItem('jwt_token', newToken);
|
||||
setIDBItem('jwt_token', newToken).catch(() => { });
|
||||
};
|
||||
const logout = async () => {
|
||||
if ('serviceWorker' in navigator) {
|
||||
|
|
@ -38,6 +40,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
}
|
||||
token.value = '';
|
||||
localStorage.removeItem('jwt_token');
|
||||
removeIDBItem('jwt_token').catch(() => { });
|
||||
};
|
||||
const decodeToken = () => {
|
||||
if (!token.value)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"auth.js","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AACnC,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAA;AAEnC,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE;IACnD,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAA;IAE1D,MAAM,eAAe,GAAG,QAAQ,CAAC,GAAG,EAAE;QACpC,IAAI,CAAC,KAAK,CAAC,KAAK;YAAE,OAAO,KAAK,CAAA;QAC9B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;YAC3D,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QACzE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAA;QACd,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,MAAM,QAAQ,GAAG,CAAC,QAAgB,EAAE,EAAE;QACpC,KAAK,CAAC,KAAK,GAAG,QAAQ,CAAA;QACtB,YAAY,CAAC,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAA;IAC7C,CAAC,CAAA;IAED,MAAM,MAAM,GAAG,KAAK,IAAI,EAAE;QACxB,IAAI,eAAe,IAAI,SAAS,EAAE,CAAC;YACjC,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,aAAa,CAAC,KAAK,CAAA;gBAC/C,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,WAAW,CAAC,eAAe,EAAE,CAAA;gBACnD,IAAI,GAAG,EAAE,CAAC;oBACR,MAAM,KAAK,CAAC,iCAAiC,EAAE;wBAC7C,MAAM,EAAE,QAAQ;wBAChB,OAAO,EAAE;4BACP,cAAc,EAAE,kBAAkB;4BAClC,eAAe,EAAE,UAAU,KAAK,CAAC,KAAK,EAAE;yBACzC;wBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;qBACjD,CAAC,CAAA;oBACF,MAAM,GAAG,CAAC,WAAW,EAAE,CAAA;gBACzB,CAAC;YACH,CAAC;YAAC,MAAM,CAAC,CAAC,gCAAgC,CAAC,CAAC;QAC9C,CAAC;QACD,KAAK,CAAC,KAAK,GAAG,EAAE,CAAA;QAChB,YAAY,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;IACtC,CAAC,CAAA;IAED,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,IAAI,CAAC,KAAK,CAAC,KAAK;YAAE,OAAO,IAAI,CAAA;QAC7B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;YACzC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAA;QAClC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC,CAAA;IAED,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,CAAA;AAClE,CAAC,CAAC,CAAA"}
|
||||
{"version":3,"file":"auth.js","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"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"}
|
||||
|
|
@ -1,14 +1,20 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, shallowRef, computed } from 'vue';
|
||||
export const useMailStore = defineStore('mail', () => {
|
||||
const mailboxes = ref([]);
|
||||
const currentMailbox = ref('');
|
||||
const folders = ref([]);
|
||||
const currentFolder = ref('');
|
||||
const messages = ref([]);
|
||||
const messages = shallowRef([]);
|
||||
const totalMessages = ref(0);
|
||||
const hasMoreMessages = ref(false);
|
||||
const messagesPage = ref(1);
|
||||
const isFetchingNextPage = ref(false);
|
||||
const currentMessage = ref(null);
|
||||
const messagesLoading = ref(false);
|
||||
const isComposeOpen = ref(false);
|
||||
const isComposeFullView = ref(false);
|
||||
const isSettingsOpen = ref(false);
|
||||
const composeDefaults = ref(null);
|
||||
const composeBody = ref('');
|
||||
const activeTab = ref('all');
|
||||
|
|
@ -16,6 +22,22 @@ export const useMailStore = defineStore('mail', () => {
|
|||
const sortOrder = ref('desc');
|
||||
const isDark = ref(localStorage.getItem('theme') === 'dark');
|
||||
const viewMode = ref(localStorage.getItem('viewMode') || 'split');
|
||||
const toast = ref(null);
|
||||
let toastTimer = null;
|
||||
let _loadMore = null;
|
||||
function showToast(message, type = 'error') {
|
||||
if (toastTimer)
|
||||
clearTimeout(toastTimer);
|
||||
toast.value = { message, type };
|
||||
toastTimer = setTimeout(() => { toast.value = null; }, 4000);
|
||||
}
|
||||
function registerLoadMore(fn) {
|
||||
_loadMore = fn;
|
||||
}
|
||||
function loadMore() {
|
||||
if (_loadMore)
|
||||
_loadMore();
|
||||
}
|
||||
const unreadMessages = computed(() => messages.value.filter((m) => !m.is_read));
|
||||
const starredMessages = computed(() => messages.value.filter((m) => m.is_starred));
|
||||
const currentFolderObj = computed(() => folders.value.find((f) => f.name === currentFolder.value) || null);
|
||||
|
|
@ -37,12 +59,15 @@ export const useMailStore = defineStore('mail', () => {
|
|||
return {
|
||||
mailboxes, currentMailbox,
|
||||
folders, currentFolder, currentFolderObj,
|
||||
messages, currentMessage,
|
||||
isComposeOpen, isComposeFullView, composeDefaults, composeBody,
|
||||
messages, totalMessages, hasMoreMessages, messagesPage, isFetchingNextPage,
|
||||
currentMessage, messagesLoading,
|
||||
isComposeOpen, isComposeFullView, isSettingsOpen, composeDefaults, composeBody,
|
||||
activeTab, isCollapsed,
|
||||
sortOrder, isDark, toggleTheme,
|
||||
viewMode, toggleViewMode,
|
||||
unreadMessages, starredMessages,
|
||||
toast, showToast,
|
||||
registerLoadMore, loadMore,
|
||||
};
|
||||
});
|
||||
//# sourceMappingURL=mail.js.map
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"mail.js","sourceRoot":"","sources":["mail.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"mail.js","sourceRoot":"","sources":["mail.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AACnC,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAA;AAG/C,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE;IACnD,MAAM,SAAS,GAAG,GAAG,CAAY,EAAE,CAAC,CAAA;IACpC,MAAM,cAAc,GAAG,GAAG,CAAS,EAAE,CAAC,CAAA;IACtC,MAAM,OAAO,GAAG,GAAG,CAAW,EAAE,CAAC,CAAA;IACjC,MAAM,aAAa,GAAG,GAAG,CAAS,EAAE,CAAC,CAAA;IACrC,MAAM,QAAQ,GAAG,UAAU,CAAY,EAAE,CAAC,CAAA;IAC1C,MAAM,aAAa,GAAG,GAAG,CAAC,CAAC,CAAC,CAAA;IAC5B,MAAM,eAAe,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IAClC,MAAM,YAAY,GAAG,GAAG,CAAC,CAAC,CAAC,CAAA;IAC3B,MAAM,kBAAkB,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IACrC,MAAM,cAAc,GAAG,GAAG,CAAiB,IAAI,CAAC,CAAA;IAChD,MAAM,eAAe,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IAClC,MAAM,aAAa,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IAChC,MAAM,iBAAiB,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IACpC,MAAM,cAAc,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IACjC,MAAM,eAAe,GAAG,GAAG,CAAyB,IAAI,CAAC,CAAA;IACzD,MAAM,WAAW,GAAG,GAAG,CAAC,EAAE,CAAC,CAAA;IAC3B,MAAM,SAAS,GAAG,GAAG,CAA+B,KAAK,CAAC,CAAA;IAC1D,MAAM,WAAW,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IAC9B,MAAM,SAAS,GAAG,GAAG,CAAiB,MAAM,CAAC,CAAA;IAC7C,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,MAAM,CAAC,CAAA;IAC5D,MAAM,QAAQ,GAAG,GAAG,CAAoB,YAAY,CAAC,OAAO,CAAC,UAAU,CAAsB,IAAI,OAAO,CAAC,CAAA;IACzG,MAAM,KAAK,GAAG,GAAG,CAAe,IAAI,CAAC,CAAA;IAErC,IAAI,UAAU,GAAyC,IAAI,CAAA;IAC3D,IAAI,SAAS,GAAwB,IAAI,CAAA;IAEzC,SAAS,SAAS,CAAC,OAAe,EAAE,OAAsB,OAAO;QAC/D,IAAI,UAAU;YAAE,YAAY,CAAC,UAAU,CAAC,CAAA;QACxC,KAAK,CAAC,KAAK,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;QAC/B,UAAU,GAAG,UAAU,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,KAAK,GAAG,IAAI,CAAA,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;IAC7D,CAAC;IAED,SAAS,gBAAgB,CAAC,EAAc;QACtC,SAAS,GAAG,EAAE,CAAA;IAChB,CAAC;IAED,SAAS,QAAQ;QACf,IAAI,SAAS;YAAE,SAAS,EAAE,CAAA;IAC5B,CAAC;IAED,MAAM,cAAc,GAAG,QAAQ,CAAC,GAAG,EAAE,CACnC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CACzC,CAAA;IAED,MAAM,eAAe,GAAG,QAAQ,CAAC,GAAG,EAAE,CACpC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAC3C,CAAA;IAED,MAAM,gBAAgB,GAAG,QAAQ,CAAC,GAAG,EAAE,CACrC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,aAAa,CAAC,KAAK,CAAC,IAAI,IAAI,CAClE,CAAA;IAED,SAAS,WAAW;QAClB,MAAM,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC,KAAK,CAAA;QAC5B,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YACjB,QAAQ,CAAC,eAAe,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YAC9C,YAAY,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;QACvC,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,eAAe,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YACjD,YAAY,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACxC,CAAC;IACH,CAAC;IAED,SAAS,cAAc;QACrB,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAA;QAC9D,YAAY,CAAC,OAAO,CAAC,UAAU,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAA;IAClD,CAAC;IAED,OAAO;QACL,SAAS,EAAE,cAAc;QACzB,OAAO,EAAE,aAAa,EAAE,gBAAgB;QACxC,QAAQ,EAAE,aAAa,EAAE,eAAe,EAAE,YAAY,EAAE,kBAAkB;QAC1E,cAAc,EAAE,eAAe;QAC/B,aAAa,EAAE,iBAAiB,EAAE,cAAc,EAAE,eAAe,EAAE,WAAW;QAC9E,SAAS,EAAE,WAAW;QACtB,SAAS,EAAE,MAAM,EAAE,WAAW;QAC9B,QAAQ,EAAE,cAAc;QACxB,cAAc,EAAE,eAAe;QAC/B,KAAK,EAAE,SAAS;QAChB,gBAAgB,EAAE,QAAQ;KAC3B,CAAA;AACH,CAAC,CAAC,CAAA"}
|
||||
2
webmail/src/types/mail.js
Normal file
2
webmail/src/types/mail.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export {};
|
||||
//# sourceMappingURL=mail.js.map
|
||||
1
webmail/src/types/mail.js.map
Normal file
1
webmail/src/types/mail.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"mail.js","sourceRoot":"","sources":["mail.ts"],"names":[],"mappings":""}
|
||||
|
|
@ -28,6 +28,7 @@ export interface Message {
|
|||
from_address: string
|
||||
to_addresses: string
|
||||
cc_addresses: string
|
||||
bcc_addresses: string
|
||||
subject: string | null
|
||||
text_body: string | null
|
||||
html_body: string | null
|
||||
|
|
@ -36,9 +37,23 @@ export interface Message {
|
|||
is_read: 0 | 1
|
||||
is_starred: 0 | 1
|
||||
is_draft: boolean
|
||||
received_via_alias: string | null
|
||||
attachments?: Attachment[]
|
||||
}
|
||||
|
||||
export interface Alias {
|
||||
address: string
|
||||
mailbox_address: string
|
||||
domain: string
|
||||
label: string | null
|
||||
description: string | null
|
||||
is_active: boolean
|
||||
expires_at: string | null
|
||||
created_at: string
|
||||
use_count: number
|
||||
last_use_at: string | null
|
||||
}
|
||||
|
||||
export interface Toast {
|
||||
message: string
|
||||
type: 'error' | 'success' | 'info'
|
||||
|
|
@ -46,6 +61,7 @@ export interface Toast {
|
|||
|
||||
export interface ComposeDefaults {
|
||||
to?: string
|
||||
from?: string
|
||||
subject?: string
|
||||
body?: string
|
||||
quotedHtml?: string
|
||||
|
|
|
|||
|
|
@ -1,26 +1,65 @@
|
|||
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
|
||||
import { onMounted, watch } from 'vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useMail } from '../composables/useMail';
|
||||
import { useMailPolling } from '../composables/useMailPolling';
|
||||
import { useNotificationsStore } from '../stores/notifications';
|
||||
import { mailApi } from '../api/mail';
|
||||
import MailLayout from '../components/mail/MailLayout.vue';
|
||||
const route = useRoute();
|
||||
const { store, loadMailboxes } = useMail();
|
||||
const mailStore = store;
|
||||
const notificationsStore = useNotificationsStore();
|
||||
useMailPolling();
|
||||
const loadMessages = async (addr, folder) => {
|
||||
const showNotifPrompt = ref(false);
|
||||
let mailboxLoadSeq = 0;
|
||||
const loadMessages = async (addr, folder, page = 1) => {
|
||||
if (!addr || !folder)
|
||||
return;
|
||||
try {
|
||||
const mRes = await mailApi.getMessages(addr, { folder, order: store.sortOrder });
|
||||
const payload = mRes.data;
|
||||
store.messages = Array.isArray(payload) ? payload : payload.items || [];
|
||||
store.currentMessage = null;
|
||||
if (page === 1) {
|
||||
store.messagesLoading = true;
|
||||
store.messages = [];
|
||||
store.messagesPage = 1;
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to load messages', e);
|
||||
else {
|
||||
store.isFetchingNextPage = true;
|
||||
}
|
||||
try {
|
||||
const mRes = await mailApi.getMessages(addr, { folder, order: store.sortOrder, page, per_page: 50 });
|
||||
const payload = mRes.data;
|
||||
const items = Array.isArray(payload) ? payload : payload.items || [];
|
||||
store.messages = page === 1 ? items : [...store.messages, ...items];
|
||||
store.totalMessages = payload.total ?? items.length;
|
||||
store.messagesPage = page;
|
||||
store.hasMoreMessages = items.length === 50;
|
||||
if (page === 1)
|
||||
store.currentMessage = null;
|
||||
}
|
||||
catch {
|
||||
store.showToast('Failed to load messages');
|
||||
}
|
||||
finally {
|
||||
store.messagesLoading = false;
|
||||
store.isFetchingNextPage = false;
|
||||
}
|
||||
};
|
||||
store.registerLoadMore(() => {
|
||||
if (store.hasMoreMessages && !store.isFetchingNextPage) {
|
||||
loadMessages(store.currentMailbox, store.currentFolder, store.messagesPage + 1);
|
||||
}
|
||||
});
|
||||
async function enableNotifications() {
|
||||
await notificationsStore.requestPermission();
|
||||
showNotifPrompt.value = false;
|
||||
localStorage.setItem('notif_prompted', '1');
|
||||
if (notificationsStore.isGranted) {
|
||||
mailStore.isSettingsOpen = true;
|
||||
}
|
||||
}
|
||||
function dismissPrompt() {
|
||||
showNotifPrompt.value = false;
|
||||
localStorage.setItem('notif_prompted', '1');
|
||||
}
|
||||
onMounted(async () => {
|
||||
await loadMailboxes();
|
||||
const mailboxParam = route.query.mailbox;
|
||||
|
|
@ -29,27 +68,46 @@ onMounted(async () => {
|
|||
if (found)
|
||||
store.currentMailbox = mailboxParam;
|
||||
}
|
||||
if (Notification.permission === 'default' && !localStorage.getItem('notif_prompted')) {
|
||||
showNotifPrompt.value = true;
|
||||
}
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('message', (ev) => {
|
||||
if (ev.data?.type === 'NOTIFICATION_CLICK' && ev.data.mailbox) {
|
||||
store.currentMailbox = ev.data.mailbox;
|
||||
}
|
||||
if (ev.data?.type === 'SET_BADGE') {
|
||||
const count = ev.data.count ?? 0;
|
||||
if ('setAppBadge' in navigator) {
|
||||
if (count > 0) {
|
||||
navigator.setAppBadge(count).catch(() => { });
|
||||
}
|
||||
else {
|
||||
navigator.clearAppBadge().catch(() => { });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
watch(() => store.currentMailbox, async (addr) => {
|
||||
if (!addr)
|
||||
return;
|
||||
const seq = ++mailboxLoadSeq;
|
||||
try {
|
||||
const fRes = await mailApi.getFolders(addr);
|
||||
if (seq !== mailboxLoadSeq)
|
||||
return;
|
||||
store.folders = fRes.data;
|
||||
if (store.folders.length > 0) {
|
||||
const inbox = store.folders.find((f) => f.name.toLowerCase() === 'inbox');
|
||||
store.currentFolder = inbox ? inbox.name : store.folders[0].name;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to load folders', e);
|
||||
catch {
|
||||
if (seq !== mailboxLoadSeq)
|
||||
return;
|
||||
store.showToast('Failed to load folders');
|
||||
}
|
||||
});
|
||||
watch(() => [store.currentMailbox, store.currentFolder], ([addr, folder]) => {
|
||||
|
|
@ -58,44 +116,154 @@ watch(() => [store.currentMailbox, store.currentFolder], ([addr, folder]) => {
|
|||
watch(() => store.sortOrder, () => {
|
||||
loadMessages(store.currentMailbox, store.currentFolder);
|
||||
});
|
||||
let openedMessageId = null;
|
||||
watch(() => store.currentMessage, async (msg) => {
|
||||
if (!msg || msg.attachments !== undefined)
|
||||
if (!msg)
|
||||
return;
|
||||
try {
|
||||
const res = await mailApi.getMessage(store.currentMailbox, msg.id);
|
||||
const fullMsg = res.data;
|
||||
store.currentMessage = fullMsg;
|
||||
const idx = store.messages.findIndex((m) => m.id === msg.id);
|
||||
if (idx !== -1) {
|
||||
store.messages[idx] = fullMsg;
|
||||
let fullMsg = msg;
|
||||
const isUserOpen = msg.attachments === undefined || msg.id !== openedMessageId;
|
||||
if (msg.attachments === undefined) {
|
||||
const res = await mailApi.getMessage(store.currentMailbox, msg.id);
|
||||
fullMsg = res.data;
|
||||
store.currentMessage = fullMsg;
|
||||
if (idx !== -1)
|
||||
store.messages[idx] = fullMsg;
|
||||
}
|
||||
if (!fullMsg.is_read) {
|
||||
if (!fullMsg.is_read && isUserOpen) {
|
||||
openedMessageId = msg.id;
|
||||
await mailApi.updateMessage(store.currentMailbox, msg.id, { is_read: true });
|
||||
if (idx !== -1) {
|
||||
store.messages[idx] = { ...store.messages[idx], is_read: 1 };
|
||||
}
|
||||
store.currentMessage = { ...store.currentMessage, is_read: 1 };
|
||||
const fRes = await mailApi.getFolders(store.currentMailbox);
|
||||
store.folders = fRes.data;
|
||||
}
|
||||
else {
|
||||
openedMessageId = msg.id;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Failed to load message', e);
|
||||
catch {
|
||||
store.showToast('Failed to load message');
|
||||
}
|
||||
});
|
||||
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
|
||||
const __VLS_ctx = {};
|
||||
let __VLS_components;
|
||||
let __VLS_directives;
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "relative h-full" },
|
||||
});
|
||||
/** @type {[typeof MailLayout, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_0 = __VLS_asFunctionalComponent(MailLayout, new MailLayout({}));
|
||||
const __VLS_1 = __VLS_0({}, ...__VLS_functionalComponentArgsRest(__VLS_0));
|
||||
var __VLS_3 = {};
|
||||
var __VLS_2;
|
||||
const __VLS_3 = {}.Transition;
|
||||
/** @type {[typeof __VLS_components.Transition, typeof __VLS_components.Transition, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_4 = __VLS_asFunctionalComponent(__VLS_3, new __VLS_3({
|
||||
name: "slide-up",
|
||||
}));
|
||||
const __VLS_5 = __VLS_4({
|
||||
name: "slide-up",
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_4));
|
||||
__VLS_6.slots.default;
|
||||
if (__VLS_ctx.showNotifPrompt) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "fixed bottom-4 left-1/2 -translate-x-1/2 z-50 flex items-center gap-4 rounded-xl border bg-background shadow-lg px-5 py-3.5 text-sm" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
|
||||
...{ class: "text-muted-foreground" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
|
||||
...{ onClick: (__VLS_ctx.enableNotifications) },
|
||||
...{ class: "inline-flex items-center justify-center rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
|
||||
...{ onClick: (__VLS_ctx.dismissPrompt) },
|
||||
...{ class: "text-muted-foreground hover:text-foreground transition-colors" },
|
||||
});
|
||||
}
|
||||
var __VLS_6;
|
||||
const __VLS_7 = {}.Transition;
|
||||
/** @type {[typeof __VLS_components.Transition, typeof __VLS_components.Transition, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_8 = __VLS_asFunctionalComponent(__VLS_7, new __VLS_7({
|
||||
name: "slide-up",
|
||||
}));
|
||||
const __VLS_9 = __VLS_8({
|
||||
name: "slide-up",
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_8));
|
||||
__VLS_10.slots.default;
|
||||
if (__VLS_ctx.store.toast) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "fixed bottom-4 right-4 z-50 flex items-center gap-3 rounded-xl border px-5 py-3.5 text-sm shadow-lg" },
|
||||
...{ class: (__VLS_ctx.store.toast.type === 'error'
|
||||
? 'bg-destructive text-destructive-foreground border-destructive'
|
||||
: __VLS_ctx.store.toast.type === 'success'
|
||||
? 'bg-green-600 text-white border-green-700'
|
||||
: 'bg-background text-foreground border-border') },
|
||||
});
|
||||
(__VLS_ctx.store.toast.message);
|
||||
}
|
||||
var __VLS_10;
|
||||
/** @type {__VLS_StyleScopedClasses['relative']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['h-full']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['fixed']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['bottom-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['left-1/2']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['-translate-x-1/2']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['z-50']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['gap-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['border']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['bg-background']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['shadow-lg']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['px-5']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['py-3.5']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['bg-primary']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['py-1.5']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-primary-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['hover:bg-primary/90']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['hover:text-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['fixed']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['bottom-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['right-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['z-50']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['border']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['px-5']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['py-3.5']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['shadow-lg']} */ ;
|
||||
var __VLS_dollars;
|
||||
const __VLS_self = (await import('vue')).defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
MailLayout: MailLayout,
|
||||
store: store,
|
||||
showNotifPrompt: showNotifPrompt,
|
||||
enableNotifications: enableNotifications,
|
||||
dismissPrompt: dismissPrompt,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,12 +1,5 @@
|
|||
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
|
||||
import { useInstallPrompt } from '@/composables/useInstallPrompt';
|
||||
import { useNotificationsStore } from '@/stores/notifications';
|
||||
import { usePushSubscription } from '@/composables/usePushSubscription';
|
||||
import { useMailStore } from '@/stores/mail';
|
||||
const { canInstall, promptInstall } = useInstallPrompt();
|
||||
const notificationsStore = useNotificationsStore();
|
||||
const push = usePushSubscription();
|
||||
const mailStore = useMailStore();
|
||||
import SettingsContent from '@/components/settings/SettingsContent.vue';
|
||||
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
|
||||
const __VLS_ctx = {};
|
||||
let __VLS_components;
|
||||
|
|
@ -14,197 +7,17 @@ let __VLS_directives;
|
|||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "p-8 max-w-2xl" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({
|
||||
...{ class: "text-2xl font-semibold mb-6" },
|
||||
});
|
||||
if (__VLS_ctx.canInstall) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "mb-4 rounded-lg border p-4 space-y-3" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
|
||||
...{ class: "text-sm font-medium" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
|
||||
...{ class: "text-sm text-muted-foreground mt-0.5" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
|
||||
...{ onClick: (__VLS_ctx.promptInstall) },
|
||||
...{ class: "inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors" },
|
||||
});
|
||||
}
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "mb-4 rounded-lg border p-4 space-y-3" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
|
||||
...{ class: "text-sm font-medium" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
|
||||
...{ class: "text-sm text-muted-foreground mt-0.5" },
|
||||
});
|
||||
if (__VLS_ctx.notificationsStore.isDenied) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
|
||||
...{ class: "text-sm text-muted-foreground" },
|
||||
});
|
||||
}
|
||||
else if (!__VLS_ctx.notificationsStore.isGranted) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
|
||||
...{ onClick: (__VLS_ctx.notificationsStore.requestPermission) },
|
||||
...{ class: "inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors" },
|
||||
});
|
||||
}
|
||||
else {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
|
||||
...{ class: "text-sm text-muted-foreground" },
|
||||
});
|
||||
if (__VLS_ctx.push.isSupported && __VLS_ctx.mailStore.currentMailbox) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "flex items-center gap-3" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
|
||||
...{ class: "text-sm" },
|
||||
});
|
||||
(__VLS_ctx.mailStore.currentMailbox);
|
||||
if (!__VLS_ctx.push.isSubscribed.value) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
|
||||
...{ onClick: (...[$event]) => {
|
||||
if (!!(__VLS_ctx.notificationsStore.isDenied))
|
||||
return;
|
||||
if (!!(!__VLS_ctx.notificationsStore.isGranted))
|
||||
return;
|
||||
if (!(__VLS_ctx.push.isSupported && __VLS_ctx.mailStore.currentMailbox))
|
||||
return;
|
||||
if (!(!__VLS_ctx.push.isSubscribed.value))
|
||||
return;
|
||||
__VLS_ctx.push.subscribe(__VLS_ctx.mailStore.currentMailbox);
|
||||
} },
|
||||
disabled: (__VLS_ctx.push.isLoading.value),
|
||||
...{ class: "inline-flex items-center justify-center rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50" },
|
||||
});
|
||||
}
|
||||
else {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
|
||||
...{ onClick: (...[$event]) => {
|
||||
if (!!(__VLS_ctx.notificationsStore.isDenied))
|
||||
return;
|
||||
if (!!(!__VLS_ctx.notificationsStore.isGranted))
|
||||
return;
|
||||
if (!(__VLS_ctx.push.isSupported && __VLS_ctx.mailStore.currentMailbox))
|
||||
return;
|
||||
if (!!(!__VLS_ctx.push.isSubscribed.value))
|
||||
return;
|
||||
__VLS_ctx.push.unsubscribe();
|
||||
} },
|
||||
disabled: (__VLS_ctx.push.isLoading.value),
|
||||
...{ class: "inline-flex items-center justify-center rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted transition-colors disabled:opacity-50" },
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (!__VLS_ctx.push.isSupported) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
|
||||
...{ class: "text-sm text-muted-foreground" },
|
||||
});
|
||||
}
|
||||
}
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
|
||||
...{ class: "text-sm text-muted-foreground" },
|
||||
});
|
||||
/** @type {[typeof SettingsContent, ]} */ ;
|
||||
// @ts-ignore
|
||||
const __VLS_0 = __VLS_asFunctionalComponent(SettingsContent, new SettingsContent({}));
|
||||
const __VLS_1 = __VLS_0({}, ...__VLS_functionalComponentArgsRest(__VLS_0));
|
||||
/** @type {__VLS_StyleScopedClasses['p-8']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['max-w-2xl']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-2xl']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['mb-6']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['border']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['space-y-3']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['mt-0.5']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['bg-primary']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-primary-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['hover:bg-primary/90']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['border']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['space-y-3']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['mt-0.5']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['bg-primary']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-primary-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['hover:bg-primary/90']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['bg-primary']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['py-1.5']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-primary-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['hover:bg-primary/90']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['disabled:opacity-50']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['border']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['py-1.5']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['hover:bg-muted']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['disabled:opacity-50']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
|
||||
var __VLS_dollars;
|
||||
const __VLS_self = (await import('vue')).defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
canInstall: canInstall,
|
||||
promptInstall: promptInstall,
|
||||
notificationsStore: notificationsStore,
|
||||
push: push,
|
||||
mailStore: mailStore,
|
||||
SettingsContent: SettingsContent,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue