mirror of
https://github.com/ChrispyBacon-dev/DockFlare.git
synced 2026-04-26 10:50:43 +00:00
PWA feature , web mail fixes, WIP push notifications
This commit is contained in:
parent
404710bee0
commit
3cedba5ac7
48 changed files with 5745 additions and 50 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import base64
|
||||
import hmac
|
||||
import ipaddress
|
||||
import json
|
||||
|
|
@ -9,7 +10,7 @@ import jwt
|
|||
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, current_app
|
||||
from flask_login import login_required, current_user
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
from cryptography.hazmat.primitives.asymmetric import ec, ed25519
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from app import config, docker_client, limiter
|
||||
from app.core import email_manager
|
||||
|
|
@ -106,7 +107,21 @@ def setup_email_domain():
|
|||
)
|
||||
email_cfg['jwt_signing_key'] = private_bytes.decode('utf-8')
|
||||
email_cfg['jwt_public_key'] = public_bytes.decode('utf-8')
|
||||
|
||||
|
||||
if 'vapid_private_key' not in email_cfg:
|
||||
vapid_key = ec.generate_private_key(ec.SECP256R1())
|
||||
email_cfg['vapid_private_key'] = vapid_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
).decode()
|
||||
email_cfg['vapid_public_key'] = base64.urlsafe_b64encode(
|
||||
vapid_key.public_key().public_bytes(
|
||||
encoding=serialization.Encoding.X962,
|
||||
format=serialization.PublicFormat.UncompressedPoint
|
||||
)
|
||||
).rstrip(b'=').decode()
|
||||
|
||||
webhook_secret = secrets.token_hex(32)
|
||||
outbound_auth_secret = secrets.token_hex(32)
|
||||
inbound_worker_name = f"dockflare-mail-inbound-{zone_name.replace('.', '-')}"
|
||||
|
|
@ -470,6 +485,23 @@ def internal_mail_config():
|
|||
cfg = config.EMAIL_CONFIG
|
||||
if not cfg or not cfg.get('enabled') or not cfg.get('domains'):
|
||||
return jsonify({'configured': False})
|
||||
|
||||
if 'vapid_private_key' not in cfg or not cfg.get('vapid_private_key'):
|
||||
vapid_key = ec.generate_private_key(ec.SECP256R1())
|
||||
cfg['vapid_private_key'] = vapid_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
).decode()
|
||||
cfg['vapid_public_key'] = base64.urlsafe_b64encode(
|
||||
vapid_key.public_key().public_bytes(
|
||||
encoding=serialization.Encoding.X962,
|
||||
format=serialization.PublicFormat.UncompressedPoint
|
||||
)
|
||||
).rstrip(b'=').decode()
|
||||
save_email_config(cfg)
|
||||
logging.info("Generated VAPID keys for existing email config")
|
||||
|
||||
domains_out = {}
|
||||
for zone_name, d in cfg['domains'].items():
|
||||
domains_out[zone_name] = {
|
||||
|
|
@ -491,6 +523,8 @@ def internal_mail_config():
|
|||
'jwt_algorithm': config.EMAIL_JWT_ALGORITHM,
|
||||
'jwt_issuer': config.EMAIL_JWT_ISSUER,
|
||||
'jwt_audience': config.EMAIL_JWT_AUDIENCE,
|
||||
'vapid_private_key': cfg.get('vapid_private_key', ''),
|
||||
'vapid_public_key': cfg.get('vapid_public_key', ''),
|
||||
'domains': domains_out
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,91 @@ def stats():
|
|||
})
|
||||
|
||||
|
||||
@api_bp.route('/notifications/vapid-key', methods=['GET'])
|
||||
@jwt_required
|
||||
def get_vapid_key():
|
||||
public_key = os.environ.get('VAPID_PUBLIC_KEY', '')
|
||||
if not public_key:
|
||||
return jsonify({"error": "push notifications not configured"}), 503
|
||||
return jsonify({"public_key": public_key})
|
||||
|
||||
|
||||
@api_bp.route('/notifications/subscribe', methods=['POST'])
|
||||
@jwt_required
|
||||
def push_subscribe():
|
||||
data = request.json or {}
|
||||
endpoint = data.get('endpoint', '')
|
||||
keys = data.get('keys') or {}
|
||||
p256dh = keys.get('p256dh', '')
|
||||
auth_key = keys.get('auth', '')
|
||||
mailbox_address = data.get('mailbox_address', '')
|
||||
|
||||
if not endpoint or not p256dh or not auth_key or not mailbox_address:
|
||||
return jsonify({"error": "endpoint, keys, and mailbox_address are required"}), 400
|
||||
|
||||
if not _check_mailbox_access(mailbox_address):
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
|
||||
db = get_db()
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
db.execute("""
|
||||
INSERT INTO push_subscriptions (mailbox_address, endpoint, p256dh, auth, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(endpoint) DO UPDATE SET
|
||||
mailbox_address=excluded.mailbox_address,
|
||||
p256dh=excluded.p256dh,
|
||||
auth=excluded.auth,
|
||||
created_at=excluded.created_at
|
||||
""", (mailbox_address, endpoint, p256dh, auth_key, now))
|
||||
db.commit()
|
||||
return jsonify({"status": "subscribed"})
|
||||
|
||||
|
||||
@api_bp.route('/notifications/subscribe', methods=['DELETE'])
|
||||
@jwt_required
|
||||
def push_unsubscribe():
|
||||
data = request.json or {}
|
||||
endpoint = data.get('endpoint', '')
|
||||
if not endpoint:
|
||||
return jsonify({"error": "endpoint is required"}), 400
|
||||
db = get_db()
|
||||
db.execute("DELETE FROM push_subscriptions WHERE endpoint=?", (endpoint,))
|
||||
db.commit()
|
||||
return jsonify({"status": "unsubscribed"})
|
||||
|
||||
|
||||
@api_bp.route('/mailboxes/status', methods=['GET'])
|
||||
@jwt_required
|
||||
def mailbox_status():
|
||||
db = get_db()
|
||||
user = request.user
|
||||
|
||||
if user.get('role') == 'admin':
|
||||
cur = db.execute("SELECT address FROM mailboxes WHERE is_active=1")
|
||||
addresses = [row['address'] for row in cur.fetchall()]
|
||||
else:
|
||||
addresses = user.get('mailboxes', [])
|
||||
|
||||
results = []
|
||||
for address in addresses:
|
||||
cur = db.execute("""
|
||||
SELECT
|
||||
COUNT(CASE WHEN m.is_read=0 THEN 1 END) AS unread_count,
|
||||
MAX(m.received_at) AS latest_received_at
|
||||
FROM messages m
|
||||
JOIN folders f ON m.folder_id = f.id
|
||||
WHERE m.mailbox_address=? AND f.name='Inbox' AND m.is_draft=0
|
||||
""", (address,))
|
||||
row = cur.fetchone()
|
||||
results.append({
|
||||
'address': address,
|
||||
'unread_count': row['unread_count'] or 0,
|
||||
'latest_received_at': row['latest_received_at'],
|
||||
})
|
||||
|
||||
return jsonify(results)
|
||||
|
||||
|
||||
@api_bp.route('/mailboxes', methods=['GET'])
|
||||
@admin_required
|
||||
def get_mailboxes():
|
||||
|
|
@ -130,6 +215,36 @@ def patch_mailbox(address):
|
|||
return jsonify({"status": "updated"})
|
||||
|
||||
|
||||
@api_bp.route('/mailboxes/<address>/preferences', methods=['GET'])
|
||||
@jwt_required
|
||||
def get_mailbox_preferences(address):
|
||||
if not _check_mailbox_access(address):
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
db = get_db()
|
||||
cur = db.execute("SELECT notification_preview FROM mailboxes WHERE address=?", (address,))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return jsonify({"error": "not found"}), 404
|
||||
preview = row['notification_preview'] if row['notification_preview'] is not None else 1
|
||||
return jsonify({"notification_preview": bool(preview)})
|
||||
|
||||
|
||||
@api_bp.route('/mailboxes/<address>/preferences', methods=['PATCH'])
|
||||
@jwt_required
|
||||
def patch_mailbox_preferences(address):
|
||||
if not _check_mailbox_access(address):
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
data = request.json or {}
|
||||
db = get_db()
|
||||
if 'notification_preview' in data:
|
||||
db.execute(
|
||||
"UPDATE mailboxes SET notification_preview=? WHERE address=?",
|
||||
(int(bool(data['notification_preview'])), address),
|
||||
)
|
||||
db.commit()
|
||||
return jsonify({"status": "updated"})
|
||||
|
||||
|
||||
@api_bp.route('/mailboxes/<address>/messages', methods=['GET'])
|
||||
@jwt_required
|
||||
def get_messages(address):
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from datetime import datetime, timezone
|
|||
from flask import Blueprint, request, jsonify
|
||||
from app.config import config
|
||||
from app.core.database import get_db
|
||||
from app.core.push import send_push_notifications
|
||||
from app.core.r2_client import fetch_email_from_r2, delete_from_r2
|
||||
from app.core.mime_parser import parse_eml
|
||||
|
||||
|
|
@ -135,6 +136,12 @@ def inbound():
|
|||
))
|
||||
|
||||
db.commit()
|
||||
send_push_notifications(to_address, {
|
||||
'message_id': msg_id,
|
||||
'subject': parsed['subject'],
|
||||
'from_name': parsed['from_name'] or parsed['from_address'],
|
||||
'mailbox': to_address,
|
||||
})
|
||||
delete_from_r2(r2_key, domain_cfg)
|
||||
|
||||
log.info("Inbound delivered: message=%s to=%s db_id=%s",
|
||||
|
|
|
|||
|
|
@ -101,6 +101,16 @@ _SCHEMA = """
|
|||
CREATE INDEX IF NOT EXISTS idx_messages_read ON messages(is_read);
|
||||
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 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mailbox_address TEXT NOT NULL,
|
||||
endpoint TEXT NOT NULL UNIQUE,
|
||||
p256dh TEXT NOT NULL,
|
||||
auth TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (mailbox_address) REFERENCES mailboxes(address) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_push_subscriptions_mailbox ON push_subscriptions(mailbox_address);
|
||||
|
||||
DROP TRIGGER IF EXISTS messages_ai;
|
||||
CREATE TRIGGER messages_ai AFTER INSERT ON messages BEGIN
|
||||
|
|
@ -144,9 +154,10 @@ def get_standalone_db():
|
|||
return _connect()
|
||||
|
||||
|
||||
def _migrate(conn):
|
||||
def _migrate(conn):
|
||||
migrations = [
|
||||
"ALTER TABLE folders ADD COLUMN color TEXT",
|
||||
"ALTER TABLE mailboxes ADD COLUMN notification_preview INTEGER DEFAULT 1",
|
||||
]
|
||||
for sql in migrations:
|
||||
try:
|
||||
|
|
|
|||
75
mail-manager/app/core/push.py
Normal file
75
mail-manager/app/core/push.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_push_notifications(mailbox_address: str, payload: dict):
|
||||
threading.Thread(
|
||||
target=_dispatch,
|
||||
args=(mailbox_address, payload),
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
|
||||
def _dispatch(mailbox_address: str, payload: dict):
|
||||
private_key = os.environ.get('VAPID_PRIVATE_KEY', '')
|
||||
if not private_key:
|
||||
return
|
||||
|
||||
from pywebpush import webpush, WebPushException
|
||||
from app.core.database import get_standalone_db
|
||||
|
||||
db = get_standalone_db()
|
||||
try:
|
||||
row = db.execute("""
|
||||
SELECT m.notification_preview,
|
||||
(SELECT COUNT(*) FROM messages msg
|
||||
JOIN folders f ON msg.folder_id = f.id
|
||||
WHERE msg.mailbox_address = m.address AND f.name='Inbox'
|
||||
AND msg.is_read=0 AND msg.is_draft=0) AS unread_count
|
||||
FROM mailboxes m WHERE m.address=?
|
||||
""", (mailbox_address,)).fetchone()
|
||||
|
||||
unread_count = row['unread_count'] if row else 0
|
||||
preview = row['notification_preview'] if row and row['notification_preview'] is not None else 1
|
||||
|
||||
push_payload = {
|
||||
'message_id': payload.get('message_id'),
|
||||
'mailbox': payload.get('mailbox'),
|
||||
'unread_count': unread_count,
|
||||
}
|
||||
if preview:
|
||||
push_payload['subject'] = payload.get('subject', '')
|
||||
push_payload['from_name'] = payload.get('from_name', '')
|
||||
|
||||
cur = db.execute(
|
||||
"SELECT id, endpoint, p256dh, auth FROM push_subscriptions WHERE mailbox_address=?",
|
||||
(mailbox_address,),
|
||||
)
|
||||
subscriptions = list(cur.fetchall())
|
||||
|
||||
for sub in subscriptions:
|
||||
try:
|
||||
webpush(
|
||||
subscription_info={
|
||||
"endpoint": sub['endpoint'],
|
||||
"keys": {"p256dh": sub['p256dh'], "auth": sub['auth']},
|
||||
},
|
||||
data=json.dumps(push_payload),
|
||||
vapid_private_key=private_key,
|
||||
vapid_claims={"sub": "mailto:push@dockflare.local"},
|
||||
)
|
||||
except WebPushException as e:
|
||||
if e.response is not None and e.response.status_code == 410:
|
||||
db.execute("DELETE FROM push_subscriptions WHERE id=?", (sub['id'],))
|
||||
db.commit()
|
||||
log.info("Removed stale push subscription for %s", mailbox_address)
|
||||
else:
|
||||
log.warning("Push failed for %s: %s", mailbox_address, e)
|
||||
except Exception:
|
||||
log.exception("Push dispatch error for %s", mailbox_address)
|
||||
finally:
|
||||
db.close()
|
||||
|
|
@ -36,6 +36,8 @@ def bootstrap():
|
|||
os.environ['JWT_ALGORITHM'] = data.get('jwt_algorithm', 'EdDSA')
|
||||
os.environ['JWT_ISSUER'] = data.get('jwt_issuer', 'dockflare-master')
|
||||
os.environ['JWT_AUDIENCE'] = data.get('jwt_audience', 'dockflare-mail')
|
||||
os.environ['VAPID_PRIVATE_KEY'] = data.get('vapid_private_key', '')
|
||||
os.environ['VAPID_PUBLIC_KEY'] = data.get('vapid_public_key', '')
|
||||
domains = data.get('domains', {})
|
||||
if not domains:
|
||||
log.warning("No domains in bootstrap config")
|
||||
|
|
|
|||
|
|
@ -7,3 +7,4 @@ cryptography==46.0.6
|
|||
python-dateutil==2.9.0.post0
|
||||
nh3==0.2.21
|
||||
requests==2.33.1
|
||||
pywebpush==2.0.0
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ server {
|
|||
listen 80;
|
||||
server_name _;
|
||||
client_max_body_size 25m;
|
||||
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data: https: blob:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self';";
|
||||
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data: https: blob:; style-src 'self' 'unsafe-inline'; script-src 'self'; worker-src 'self'; connect-src 'self';";
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://dockflare-mail-manager:8025/api/;
|
||||
|
|
@ -24,6 +24,18 @@ server {
|
|||
proxy_set_header CF-Connecting-IP \$http_cf_connecting_ip;
|
||||
}
|
||||
|
||||
location = /sw.js {
|
||||
root /usr/share/nginx/html;
|
||||
add_header Cache-Control "no-cache";
|
||||
add_header Service-Worker-Allowed "/";
|
||||
}
|
||||
|
||||
location = /manifest.webmanifest {
|
||||
root /usr/share/nginx/html;
|
||||
add_header Cache-Control "no-cache";
|
||||
types { application/manifest+json webmanifest; }
|
||||
}
|
||||
|
||||
location = /config.json {
|
||||
root /usr/share/nginx/html;
|
||||
add_header Cache-Control "no-store";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DockFlare Webmail</title>
|
||||
<meta name="theme-color" content="#0f172a" />
|
||||
<title>DockFlare Mail</title>
|
||||
<link rel="apple-touch-icon" href="/favicon/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/favicon/android-chrome-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="512x512" href="/favicon/android-chrome-512x512.png">
|
||||
|
|
|
|||
4434
webmail/package-lock.json
generated
4434
webmail/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -39,6 +39,9 @@
|
|||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.5.0",
|
||||
"vite": "^5.4.0",
|
||||
"vue-tsc": "^2.1.0"
|
||||
"vite-plugin-pwa": "^0.21.0",
|
||||
"vue-tsc": "^2.1.0",
|
||||
"workbox-precaching": "^7.3.0",
|
||||
"workbox-routing": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
36
webmail/public/offline.html
Normal file
36
webmail/public/offline.html
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Offline — DockFlare Mail</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0f172a;
|
||||
color: #f8fafc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.card {
|
||||
text-align: center;
|
||||
padding: 2.5rem 2rem;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.5rem;
|
||||
max-width: 340px;
|
||||
width: 100%;
|
||||
}
|
||||
h1 { font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; }
|
||||
p { font-size: 0.875rem; color: #94a3b8; line-height: 1.5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>You're offline</h1>
|
||||
<p>DockFlare Mail will reconnect when your network is available.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,7 +1,24 @@
|
|||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
import { useRegisterSW } from 'virtual:pwa-register/vue'
|
||||
|
||||
const { needRefresh, updateServiceWorker } = useRegisterSW()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
<Transition name="slide-up">
|
||||
<div
|
||||
v-if="needRefresh"
|
||||
class="fixed bottom-4 right-4 z-50 flex items-center gap-3 rounded-lg border bg-card px-4 py-3 shadow-lg text-card-foreground"
|
||||
>
|
||||
<span class="text-sm">Update available</span>
|
||||
<button
|
||||
class="text-sm font-medium underline underline-offset-2"
|
||||
@click="updateServiceWorker()"
|
||||
>
|
||||
Reload
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
/// <reference types="../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
|
||||
import { RouterView } from 'vue-router';
|
||||
import { useRegisterSW } from 'virtual:pwa-register/vue';
|
||||
const { needRefresh, updateServiceWorker } = useRegisterSW();
|
||||
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
|
||||
const __VLS_ctx = {};
|
||||
let __VLS_components;
|
||||
|
|
@ -9,13 +11,59 @@ const __VLS_0 = {}.RouterView;
|
|||
// @ts-ignore
|
||||
const __VLS_1 = __VLS_asFunctionalComponent(__VLS_0, new __VLS_0({}));
|
||||
const __VLS_2 = __VLS_1({}, ...__VLS_functionalComponentArgsRest(__VLS_1));
|
||||
var __VLS_4 = {};
|
||||
var __VLS_3;
|
||||
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: "slide-up",
|
||||
}));
|
||||
const __VLS_6 = __VLS_5({
|
||||
name: "slide-up",
|
||||
}, ...__VLS_functionalComponentArgsRest(__VLS_5));
|
||||
__VLS_7.slots.default;
|
||||
if (__VLS_ctx.needRefresh) {
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "fixed bottom-4 right-4 z-50 flex items-center gap-3 rounded-lg border bg-card px-4 py-3 shadow-lg text-card-foreground" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
|
||||
...{ class: "text-sm" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
|
||||
...{ onClick: (...[$event]) => {
|
||||
if (!(__VLS_ctx.needRefresh))
|
||||
return;
|
||||
__VLS_ctx.updateServiceWorker();
|
||||
} },
|
||||
...{ class: "text-sm font-medium underline underline-offset-2" },
|
||||
});
|
||||
}
|
||||
var __VLS_7;
|
||||
/** @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-lg']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['border']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['bg-card']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['py-3']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['shadow-lg']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-card-foreground']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['underline']} */ ;
|
||||
/** @type {__VLS_StyleScopedClasses['underline-offset-2']} */ ;
|
||||
var __VLS_dollars;
|
||||
const __VLS_self = (await import('vue')).defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
RouterView: RouterView,
|
||||
needRefresh: needRefresh,
|
||||
updateServiceWorker: updateServiceWorker,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"App.vue.js","sourceRoot":"","sources":["App.vue"],"names":[],"mappings":"AAMW,8EAA8E;AAEzF,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AACvC,QAAQ,CAAA,CAAA,yCAAyC;AAIjD,MAAM,SAAS,GAAG,EAAqE,CAAC;AAExF,IAAI,gBAAiE,CAAC;AAEtE,IAAI,gBAAiE,CAAC;AACtE,MAAM,OAAO,GAAI,EAA+G,CAAC,UAAU,CAAC;AAC5I,qDAAqD,CAAA,CAAC;AACtD,aAAa;AACb,MAAM,OAAO,GAAG,2BAA2B,CAAC,OAAO,EAAE,IAAI,OAAO,CAAC,EAChE,CAAC,CAAC,CAAC;AACJ,MAAM,OAAO,GAAG,OAAO,CAAC,EACvB,EAAE,GAAG,iCAAiC,CAAC,OAAO,CAAC,CAAC,CAAC;AAClD,IAAI,OAAO,GAAG,EAAmE,CAAC;AAClF,IAAI,OAA0E,CAAC;AAO/E,IAAI,aAK+D,CAAC;AACpE,MAAM,UAAU,GAAG,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,eAAe,CAAC;IACzD,KAAK;QACL,OAAO;YACP,UAAU,EAAE,UAA+B;SAC1C,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"}
|
||||
{"version":3,"file":"App.vue.js","sourceRoot":"","sources":["App.vue"],"names":[],"mappings":"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"}
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import apiClient from './client';
|
||||
export const authApi = {
|
||||
checkAuth: () => apiClient.get('/auth/me'),
|
||||
loginWithPassword: async (baseUrl, email, password) => {
|
||||
const url = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
||||
const response = await fetch(`${url}/email/auth/login`, {
|
||||
loginWithPassword: async (email, password) => {
|
||||
const response = await fetch('/email/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
|
|
|
|||
|
|
@ -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,OAAe,EAAE,KAAa,EAAE,QAAgB,EAAE,EAAE;QAC5E,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;QAClE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,GAAG,mBAAmB,EAAE;YACtD,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;CACF,CAAA"}
|
||||
|
|
@ -14,6 +14,7 @@ export const mailApi = {
|
|||
markMessages: (address, data) => apiClient.post(`/mailboxes/${address}/messages/mark`, data),
|
||||
sendMessage: (address, data) => apiClient.post(`/mailboxes/${address}/send`, data),
|
||||
searchMessages: (address, params) => apiClient.get(`/mailboxes/${address}/search`, { params }),
|
||||
getMailboxStatus: () => apiClient.get('/mailboxes/status'),
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -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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"}
|
||||
|
|
@ -20,6 +20,10 @@ export const mailApi = {
|
|||
sendMessage: (address: string, data: FormData | Record<string, any>) =>
|
||||
apiClient.post(`/mailboxes/${address}/send`, data),
|
||||
searchMessages: (address: string, params: any) => apiClient.get(`/mailboxes/${address}/search`, { params }),
|
||||
getMailboxStatus: () => apiClient.get('/mailboxes/status'),
|
||||
getMailboxPreferences: (address: string) => apiClient.get(`/mailboxes/${address}/preferences`),
|
||||
updateMailboxPreferences: (address: string, data: Record<string, any>) =>
|
||||
apiClient.patch(`/mailboxes/${address}/preferences`, data),
|
||||
getAttachmentUrl: (id: string) => `/api/v1/attachments/${id}/download`,
|
||||
downloadAttachment: (id: number | string) =>
|
||||
apiClient.get(`/attachments/${id}/download`, { responseType: 'blob' }).then(r => r.data as Blob),
|
||||
|
|
|
|||
26
webmail/src/composables/useInstallPrompt.js
Normal file
26
webmail/src/composables/useInstallPrompt.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { ref } from 'vue';
|
||||
const deferredPrompt = ref(null);
|
||||
const canInstall = ref(false);
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
e.preventDefault();
|
||||
deferredPrompt.value = e;
|
||||
canInstall.value = true;
|
||||
});
|
||||
window.addEventListener('appinstalled', () => {
|
||||
deferredPrompt.value = null;
|
||||
canInstall.value = false;
|
||||
});
|
||||
export function useInstallPrompt() {
|
||||
const promptInstall = async () => {
|
||||
if (!deferredPrompt.value)
|
||||
return;
|
||||
await deferredPrompt.value.prompt();
|
||||
const { outcome } = await deferredPrompt.value.userChoice;
|
||||
if (outcome === 'accepted') {
|
||||
deferredPrompt.value = null;
|
||||
canInstall.value = false;
|
||||
}
|
||||
};
|
||||
return { canInstall, promptInstall };
|
||||
}
|
||||
//# sourceMappingURL=useInstallPrompt.js.map
|
||||
1
webmail/src/composables/useInstallPrompt.js.map
Normal file
1
webmail/src/composables/useInstallPrompt.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"useInstallPrompt.js","sourceRoot":"","sources":["useInstallPrompt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAOzB,MAAM,cAAc,GAAG,GAAG,CAAkC,IAAI,CAAC,CAAA;AACjE,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;AAE7B,MAAM,CAAC,gBAAgB,CAAC,qBAAqB,EAAE,CAAC,CAAC,EAAE,EAAE;IACnD,CAAC,CAAC,cAAc,EAAE,CAAA;IAClB,cAAc,CAAC,KAAK,GAAG,CAA6B,CAAA;IACpD,UAAU,CAAC,KAAK,GAAG,IAAI,CAAA;AACzB,CAAC,CAAC,CAAA;AAEF,MAAM,CAAC,gBAAgB,CAAC,cAAc,EAAE,GAAG,EAAE;IAC3C,cAAc,CAAC,KAAK,GAAG,IAAI,CAAA;IAC3B,UAAU,CAAC,KAAK,GAAG,KAAK,CAAA;AAC1B,CAAC,CAAC,CAAA;AAEF,MAAM,UAAU,gBAAgB;IAC9B,MAAM,aAAa,GAAG,KAAK,IAAI,EAAE;QAC/B,IAAI,CAAC,cAAc,CAAC,KAAK;YAAE,OAAM;QACjC,MAAM,cAAc,CAAC,KAAK,CAAC,MAAM,EAAE,CAAA;QACnC,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,cAAc,CAAC,KAAK,CAAC,UAAU,CAAA;QACzD,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;YAC3B,cAAc,CAAC,KAAK,GAAG,IAAI,CAAA;YAC3B,UAAU,CAAC,KAAK,GAAG,KAAK,CAAA;QAC1B,CAAC;IACH,CAAC,CAAA;IAED,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,CAAA;AACtC,CAAC"}
|
||||
34
webmail/src/composables/useInstallPrompt.ts
Normal file
34
webmail/src/composables/useInstallPrompt.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { ref } from 'vue'
|
||||
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
prompt(): Promise<void>
|
||||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>
|
||||
}
|
||||
|
||||
const deferredPrompt = ref<BeforeInstallPromptEvent | null>(null)
|
||||
const canInstall = ref(false)
|
||||
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
e.preventDefault()
|
||||
deferredPrompt.value = e as BeforeInstallPromptEvent
|
||||
canInstall.value = true
|
||||
})
|
||||
|
||||
window.addEventListener('appinstalled', () => {
|
||||
deferredPrompt.value = null
|
||||
canInstall.value = false
|
||||
})
|
||||
|
||||
export function useInstallPrompt() {
|
||||
const promptInstall = async () => {
|
||||
if (!deferredPrompt.value) return
|
||||
await deferredPrompt.value.prompt()
|
||||
const { outcome } = await deferredPrompt.value.userChoice
|
||||
if (outcome === 'accepted') {
|
||||
deferredPrompt.value = null
|
||||
canInstall.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { canInstall, promptInstall }
|
||||
}
|
||||
|
|
@ -1,17 +1,26 @@
|
|||
import { ref } from 'vue';
|
||||
import { mailApi } from '../api/mail';
|
||||
import { useMailStore } from '../stores/mail';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
export function useMail() {
|
||||
const store = useMailStore();
|
||||
const authStore = useAuthStore();
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
const loadMailboxes = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await mailApi.getMailboxes();
|
||||
store.mailboxes = res.data;
|
||||
if (res.data.length > 0 && !store.currentMailbox) {
|
||||
store.currentMailbox = res.data[0].address;
|
||||
const decoded = authStore.decodeToken();
|
||||
if (decoded?.role === 'user') {
|
||||
const addresses = decoded.mailboxes || [];
|
||||
store.mailboxes = addresses.map((addr) => ({ address: addr, display_name: addr }));
|
||||
}
|
||||
else {
|
||||
const res = await mailApi.getMailboxes();
|
||||
store.mailboxes = res.data;
|
||||
}
|
||||
if (store.mailboxes.length > 0 && !store.currentMailbox) {
|
||||
store.currentMailbox = store.mailboxes[0].address;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"useMail.js","sourceRoot":"","sources":["useMail.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AACzB,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AACrC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE7C,MAAM,UAAU,OAAO;IACrB,MAAM,KAAK,GAAG,YAAY,EAAE,CAAA;IAC5B,MAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IAC1B,MAAM,KAAK,GAAG,GAAG,CAAC,EAAE,CAAC,CAAA;IAErB,MAAM,aAAa,GAAG,KAAK,IAAI,EAAE;QAC/B,OAAO,CAAC,KAAK,GAAG,IAAI,CAAA;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,YAAY,EAAE,CAAA;YACxC,KAAK,CAAC,SAAS,GAAG,GAAG,CAAC,IAAI,CAAA;YAC1B,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC;gBACjD,KAAK,CAAC,cAAc,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;YAC5C,CAAC;QACH,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,OAAO,CAAA;QACzB,CAAC;gBAAS,CAAC;YACT,OAAO,CAAC,KAAK,GAAG,KAAK,CAAA;QACvB,CAAC;IACH,CAAC,CAAA;IAED,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,CAAA;AACjD,CAAC"}
|
||||
{"version":3,"file":"useMail.js","sourceRoot":"","sources":["useMail.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AACzB,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AACrC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE7C,MAAM,UAAU,OAAO;IACrB,MAAM,KAAK,GAAG,YAAY,EAAE,CAAA;IAC5B,MAAM,SAAS,GAAG,YAAY,EAAE,CAAA;IAChC,MAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IAC1B,MAAM,KAAK,GAAG,GAAG,CAAC,EAAE,CAAC,CAAA;IAErB,MAAM,aAAa,GAAG,KAAK,IAAI,EAAE;QAC/B,OAAO,CAAC,KAAK,GAAG,IAAI,CAAA;QACpB,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,SAAS,CAAC,WAAW,EAAE,CAAA;YACvC,IAAI,OAAO,EAAE,IAAI,KAAK,MAAM,EAAE,CAAC;gBAC7B,MAAM,SAAS,GAAa,OAAO,CAAC,SAAS,IAAI,EAAE,CAAA;gBACnD,KAAK,CAAC,SAAS,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;YAC5F,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,YAAY,EAAE,CAAA;gBACxC,KAAK,CAAC,SAAS,GAAG,GAAG,CAAC,IAAI,CAAA;YAC5B,CAAC;YACD,IAAI,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC;gBACxD,KAAK,CAAC,cAAc,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;YACnD,CAAC;QACH,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,OAAO,CAAA;QACzB,CAAC;gBAAS,CAAC;YACT,OAAO,CAAC,KAAK,GAAG,KAAK,CAAA;QACvB,CAAC;IACH,CAAC,CAAA;IAED,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,CAAA;AACjD,CAAC"}
|
||||
58
webmail/src/composables/useMailPolling.js
Normal file
58
webmail/src/composables/useMailPolling.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { onUnmounted, ref, watch } from 'vue';
|
||||
import { mailApi } from '@/api/mail';
|
||||
import { useNotificationsStore } from '@/stores/notifications';
|
||||
import { useMailStore } from '@/stores/mail';
|
||||
export function useMailPolling() {
|
||||
const notificationsStore = useNotificationsStore();
|
||||
const mailStore = useMailStore();
|
||||
const lastSeen = ref({});
|
||||
const initialized = ref(false);
|
||||
const poll = async () => {
|
||||
if (mailStore.mailboxes.length === 0)
|
||||
return;
|
||||
try {
|
||||
const res = await mailApi.getMailboxStatus();
|
||||
const statuses = res.data;
|
||||
if (!initialized.value) {
|
||||
for (const s of statuses) {
|
||||
lastSeen.value[s.address] = s.latest_received_at;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// network error — skip
|
||||
}
|
||||
};
|
||||
const fireNotification = (address, unreadCount) => {
|
||||
const n = new Notification(address, {
|
||||
body: `${unreadCount} unread message${unreadCount !== 1 ? 's' : ''}`,
|
||||
icon: '/favicon/android-chrome-192x192.png',
|
||||
tag: address,
|
||||
data: { mailbox: address },
|
||||
});
|
||||
n.onclick = () => {
|
||||
window.focus();
|
||||
mailStore.currentMailbox = address;
|
||||
n.close();
|
||||
};
|
||||
};
|
||||
watch(() => mailStore.mailboxes, (boxes) => {
|
||||
if (boxes.length > 0 && !initialized.value)
|
||||
poll();
|
||||
}, { immediate: true });
|
||||
const interval = setInterval(poll, 60_000);
|
||||
onUnmounted(() => clearInterval(interval));
|
||||
}
|
||||
//# sourceMappingURL=useMailPolling.js.map
|
||||
1
webmail/src/composables/useMailPolling.js.map
Normal file
1
webmail/src/composables/useMailPolling.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"useMailPolling.js","sourceRoot":"","sources":["useMailPolling.ts"],"names":[],"mappings":"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"}
|
||||
86
webmail/src/composables/useMailPolling.ts
Normal file
86
webmail/src/composables/useMailPolling.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { onUnmounted, ref, watch } from 'vue'
|
||||
import { mailApi } from '@/api/mail'
|
||||
import { useNotificationsStore } from '@/stores/notifications'
|
||||
import { useMailStore } from '@/stores/mail'
|
||||
|
||||
interface MailboxStatus {
|
||||
address: string
|
||||
unread_count: number
|
||||
latest_received_at: string | null
|
||||
}
|
||||
|
||||
function updateBadge(count: number) {
|
||||
if (!('setAppBadge' in navigator)) return
|
||||
if (count > 0) {
|
||||
navigator.setAppBadge(count).catch(() => {})
|
||||
} else {
|
||||
navigator.clearAppBadge().catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
export function useMailPolling() {
|
||||
const notificationsStore = useNotificationsStore()
|
||||
const mailStore = useMailStore()
|
||||
const lastSeen = ref<Record<string, string | null>>({})
|
||||
const initialized = ref(false)
|
||||
|
||||
const poll = async () => {
|
||||
if (mailStore.mailboxes.length === 0) return
|
||||
|
||||
try {
|
||||
const res = await mailApi.getMailboxStatus()
|
||||
const statuses: MailboxStatus[] = 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
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// network error — skip
|
||||
}
|
||||
}
|
||||
|
||||
const fireNotification = (address: string, unreadCount: number) => {
|
||||
const n = new Notification(address, {
|
||||
body: `${unreadCount} unread message${unreadCount !== 1 ? 's' : ''}`,
|
||||
icon: '/favicon/android-chrome-192x192.png',
|
||||
tag: address,
|
||||
data: { mailbox: address },
|
||||
})
|
||||
n.onclick = () => {
|
||||
window.focus()
|
||||
mailStore.currentMailbox = address
|
||||
n.close()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => mailStore.mailboxes,
|
||||
(boxes) => {
|
||||
if (boxes.length > 0 && !initialized.value) poll()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const interval = setInterval(poll, 60_000)
|
||||
onUnmounted(() => clearInterval(interval))
|
||||
}
|
||||
67
webmail/src/composables/usePushSubscription.js
Normal file
67
webmail/src/composables/usePushSubscription.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { ref } from 'vue';
|
||||
import apiClient from '@/api/client';
|
||||
const isSupported = typeof window !== 'undefined' && 'serviceWorker' in navigator && 'PushManager' in window;
|
||||
function urlBase64ToUint8Array(base64) {
|
||||
const padding = '='.repeat((4 - (base64.length % 4)) % 4);
|
||||
const raw = atob((base64 + padding).replace(/-/g, '+').replace(/_/g, '/'));
|
||||
const bytes = new Uint8Array(raw.length);
|
||||
for (let i = 0; i < raw.length; i++)
|
||||
bytes[i] = raw.charCodeAt(i);
|
||||
return bytes;
|
||||
}
|
||||
export function usePushSubscription() {
|
||||
const isSubscribed = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const checkSubscription = async () => {
|
||||
if (!isSupported)
|
||||
return;
|
||||
const reg = await navigator.serviceWorker.ready;
|
||||
const sub = await reg.pushManager.getSubscription();
|
||||
isSubscribed.value = !!sub;
|
||||
};
|
||||
const subscribe = async (mailboxAddress) => {
|
||||
if (!isSupported)
|
||||
return;
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const { data } = await apiClient.get('/notifications/vapid-key');
|
||||
const reg = await navigator.serviceWorker.ready;
|
||||
const sub = await reg.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(data.public_key),
|
||||
});
|
||||
const subJson = sub.toJSON();
|
||||
await apiClient.post('/notifications/subscribe', {
|
||||
endpoint: subJson.endpoint,
|
||||
keys: subJson.keys,
|
||||
mailbox_address: mailboxAddress,
|
||||
});
|
||||
isSubscribed.value = true;
|
||||
}
|
||||
finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
const unsubscribe = async () => {
|
||||
if (!isSupported)
|
||||
return;
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const reg = await navigator.serviceWorker.ready;
|
||||
const sub = await reg.pushManager.getSubscription();
|
||||
if (sub) {
|
||||
await apiClient.delete('/notifications/subscribe', {
|
||||
data: { endpoint: sub.endpoint },
|
||||
});
|
||||
await sub.unsubscribe();
|
||||
}
|
||||
isSubscribed.value = false;
|
||||
}
|
||||
finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
checkSubscription();
|
||||
return { isSubscribed, isLoading, isSupported, subscribe, unsubscribe };
|
||||
}
|
||||
//# sourceMappingURL=usePushSubscription.js.map
|
||||
1
webmail/src/composables/usePushSubscription.js.map
Normal file
1
webmail/src/composables/usePushSubscription.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"usePushSubscription.js","sourceRoot":"","sources":["usePushSubscription.ts"],"names":[],"mappings":"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"}
|
||||
68
webmail/src/composables/usePushSubscription.ts
Normal file
68
webmail/src/composables/usePushSubscription.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { ref } from 'vue'
|
||||
import apiClient from '@/api/client'
|
||||
|
||||
const isSupported = typeof window !== 'undefined' && 'serviceWorker' in navigator && 'PushManager' in window
|
||||
|
||||
function urlBase64ToUint8Array(base64: string): Uint8Array<ArrayBuffer> {
|
||||
const padding = '='.repeat((4 - (base64.length % 4)) % 4)
|
||||
const raw = atob((base64 + padding).replace(/-/g, '+').replace(/_/g, '/'))
|
||||
const bytes = new Uint8Array(raw.length)
|
||||
for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i)
|
||||
return bytes
|
||||
}
|
||||
|
||||
export function usePushSubscription() {
|
||||
const isSubscribed = ref(false)
|
||||
const isLoading = ref(false)
|
||||
|
||||
const checkSubscription = async () => {
|
||||
if (!isSupported) return
|
||||
const reg = await navigator.serviceWorker.ready
|
||||
const sub = await reg.pushManager.getSubscription()
|
||||
isSubscribed.value = !!sub
|
||||
}
|
||||
|
||||
const subscribe = async (mailboxAddress: string) => {
|
||||
if (!isSupported) return
|
||||
isLoading.value = true
|
||||
try {
|
||||
const { data } = await apiClient.get('/notifications/vapid-key')
|
||||
const reg = await navigator.serviceWorker.ready
|
||||
const sub = await reg.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(data.public_key),
|
||||
})
|
||||
const subJson = sub.toJSON()
|
||||
await apiClient.post('/notifications/subscribe', {
|
||||
endpoint: subJson.endpoint,
|
||||
keys: subJson.keys,
|
||||
mailbox_address: mailboxAddress,
|
||||
})
|
||||
isSubscribed.value = true
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const unsubscribe = async () => {
|
||||
if (!isSupported) return
|
||||
isLoading.value = true
|
||||
try {
|
||||
const reg = await navigator.serviceWorker.ready
|
||||
const sub = await reg.pushManager.getSubscription()
|
||||
if (sub) {
|
||||
await apiClient.delete('/notifications/subscribe', {
|
||||
data: { endpoint: sub.endpoint },
|
||||
})
|
||||
await sub.unsubscribe()
|
||||
}
|
||||
isSubscribed.value = false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
checkSubscription()
|
||||
|
||||
return { isSubscribed, isLoading, isSupported, subscribe, unsubscribe }
|
||||
}
|
||||
|
|
@ -17,7 +17,25 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
token.value = newToken;
|
||||
localStorage.setItem('jwt_token', newToken);
|
||||
};
|
||||
const logout = () => {
|
||||
const logout = async () => {
|
||||
if ('serviceWorker' in navigator) {
|
||||
try {
|
||||
const reg = await navigator.serviceWorker.ready;
|
||||
const sub = await reg.pushManager.getSubscription();
|
||||
if (sub) {
|
||||
await fetch('/api/v1/notifications/subscribe', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token.value}`,
|
||||
},
|
||||
body: JSON.stringify({ endpoint: sub.endpoint }),
|
||||
});
|
||||
await sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
catch { /* ignore push cleanup errors */ }
|
||||
}
|
||||
token.value = '';
|
||||
localStorage.removeItem('jwt_token');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,GAAG,EAAE;QAClB,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":"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"}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
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(() => {
|
||||
if (!token.value) return false
|
||||
try {
|
||||
|
|
@ -13,15 +14,34 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
return false
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const setToken = (newToken: string) => {
|
||||
token.value = newToken
|
||||
localStorage.setItem('jwt_token', newToken)
|
||||
setIDBItem('jwt_token', newToken).catch(() => {})
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
|
||||
const logout = async () => {
|
||||
if ('serviceWorker' in navigator) {
|
||||
try {
|
||||
const reg = await navigator.serviceWorker.ready
|
||||
const sub = await reg.pushManager.getSubscription()
|
||||
if (sub) {
|
||||
await fetch('/api/v1/notifications/subscribe', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token.value}`,
|
||||
},
|
||||
body: JSON.stringify({ endpoint: sub.endpoint }),
|
||||
})
|
||||
await sub.unsubscribe()
|
||||
}
|
||||
} catch { /* ignore push cleanup errors */ }
|
||||
}
|
||||
token.value = ''
|
||||
localStorage.removeItem('jwt_token')
|
||||
removeIDBItem('jwt_token').catch(() => {})
|
||||
}
|
||||
|
||||
const decodeToken = () => {
|
||||
|
|
@ -35,4 +55,4 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
}
|
||||
|
||||
return { token, isAuthenticated, setToken, logout, decodeToken }
|
||||
})
|
||||
})
|
||||
|
|
|
|||
15
webmail/src/stores/notifications.js
Normal file
15
webmail/src/stores/notifications.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
export const useNotificationsStore = defineStore('notifications', () => {
|
||||
const permission = ref(typeof Notification !== 'undefined' ? Notification.permission : 'denied');
|
||||
const isGranted = computed(() => permission.value === 'granted');
|
||||
const isDenied = computed(() => permission.value === 'denied');
|
||||
async function requestPermission() {
|
||||
if (typeof Notification === 'undefined')
|
||||
return;
|
||||
const result = await Notification.requestPermission();
|
||||
permission.value = result;
|
||||
}
|
||||
return { permission, isGranted, isDenied, requestPermission };
|
||||
});
|
||||
//# sourceMappingURL=notifications.js.map
|
||||
1
webmail/src/stores/notifications.js.map
Normal file
1
webmail/src/stores/notifications.js.map
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"notifications.js","sourceRoot":"","sources":["notifications.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,qBAAqB,GAAG,WAAW,CAAC,eAAe,EAAE,GAAG,EAAE;IACrE,MAAM,UAAU,GAAG,GAAG,CACpB,OAAO,YAAY,KAAK,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CACzE,CAAA;IAED,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,KAAK,SAAS,CAAC,CAAA;IAChE,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAA;IAE9D,KAAK,UAAU,iBAAiB;QAC9B,IAAI,OAAO,YAAY,KAAK,WAAW;YAAE,OAAM;QAC/C,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,iBAAiB,EAAE,CAAA;QACrD,UAAU,CAAC,KAAK,GAAG,MAAM,CAAA;IAC3B,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,iBAAiB,EAAE,CAAA;AAC/D,CAAC,CAAC,CAAA"}
|
||||
19
webmail/src/stores/notifications.ts
Normal file
19
webmail/src/stores/notifications.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useNotificationsStore = defineStore('notifications', () => {
|
||||
const permission = ref<NotificationPermission>(
|
||||
typeof Notification !== 'undefined' ? Notification.permission : 'denied'
|
||||
)
|
||||
|
||||
const isGranted = computed(() => permission.value === 'granted')
|
||||
const isDenied = computed(() => permission.value === 'denied')
|
||||
|
||||
async function requestPermission() {
|
||||
if (typeof Notification === 'undefined') return
|
||||
const result = await Notification.requestPermission()
|
||||
permission.value = result
|
||||
}
|
||||
|
||||
return { permission, isGranted, isDenied, requestPermission }
|
||||
})
|
||||
89
webmail/src/sw.ts
Normal file
89
webmail/src/sw.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
/// <reference lib="webworker" />
|
||||
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching'
|
||||
import { setCatchHandler } from 'workbox-routing'
|
||||
import { getIDBItem } from './lib/idb'
|
||||
|
||||
declare const self: ServiceWorkerGlobalScope & {
|
||||
__WB_MANIFEST: Array<{ url: string; revision: string | null }>
|
||||
}
|
||||
|
||||
cleanupOutdatedCaches()
|
||||
precacheAndRoute(self.__WB_MANIFEST)
|
||||
|
||||
async function broadcastBadge(count: number) {
|
||||
const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true })
|
||||
for (const client of clients) {
|
||||
client.postMessage({ type: 'SET_BADGE', count })
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener('push', (event) => {
|
||||
const data = event.data?.json() ?? {}
|
||||
const unreadCount: number = data.unread_count ?? 0
|
||||
|
||||
const notifOptions: NotificationOptions = {
|
||||
body: data.subject ?? 'New message',
|
||||
icon: '/favicon/android-chrome-192x192.png',
|
||||
badge: '/favicon/favicon-32x32.png',
|
||||
tag: String(data.message_id ?? Date.now()),
|
||||
data: { messageId: data.message_id, mailbox: data.mailbox, unreadCount },
|
||||
actions: [{ action: 'mark-read', title: 'Mark as Read' }],
|
||||
}
|
||||
|
||||
event.waitUntil(
|
||||
self.registration
|
||||
.showNotification(data.mailbox ?? 'DockFlare Mail', notifOptions)
|
||||
.then(() => broadcastBadge(unreadCount))
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
event.notification.close()
|
||||
const { messageId, mailbox, unreadCount } = event.notification.data ?? {}
|
||||
|
||||
if (event.action === 'mark-read' && messageId && mailbox) {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
const token = await getIDBItem('jwt_token')
|
||||
if (token) {
|
||||
try {
|
||||
await fetch(`/api/v1/mailboxes/${mailbox}/messages/${messageId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ is_read: true }),
|
||||
})
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
await broadcastBadge(Math.max(0, (unreadCount ?? 1) - 1))
|
||||
})()
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
event.waitUntil(
|
||||
self.clients
|
||||
.matchAll({ type: 'window', includeUncontrolled: true })
|
||||
.then((clients) => {
|
||||
for (const client of clients) {
|
||||
if ('focus' in client) {
|
||||
client.postMessage({ type: 'NOTIFICATION_CLICK', messageId, mailbox })
|
||||
return (client as WindowClient).focus()
|
||||
}
|
||||
}
|
||||
const url = mailbox
|
||||
? `/?mailbox=${encodeURIComponent(mailbox)}&message=${encodeURIComponent(messageId ?? '')}`
|
||||
: '/'
|
||||
return self.clients.openWindow(url)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
setCatchHandler(async ({ request }) => {
|
||||
if (request.destination === 'document') {
|
||||
return (await caches.match('/offline.html')) ?? Response.error()
|
||||
}
|
||||
return Response.error()
|
||||
})
|
||||
|
|
@ -31,8 +31,7 @@ const handleLogin = async () => {
|
|||
error.value = '';
|
||||
loading.value = true;
|
||||
try {
|
||||
const masterUrl = await getMasterUrl();
|
||||
const data = await authApi.loginWithPassword(masterUrl, email.value, password.value);
|
||||
const data = await authApi.loginWithPassword(email.value, password.value);
|
||||
if (data.success && data.token) {
|
||||
login(data.token);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,10 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useMail } from '../composables/useMail'
|
||||
import { useMailPolling } from '../composables/useMailPolling'
|
||||
import { mailApi } from '../api/mail'
|
||||
import MailLayout from '../components/mail/MailLayout.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const { store, loadMailboxes } = useMail()
|
||||
useMailPolling()
|
||||
|
||||
const loadMessages = async (addr: string, folder: string) => {
|
||||
if (!addr || !folder) return
|
||||
|
|
@ -18,8 +22,32 @@ const loadMessages = async (addr: string, folder: string) => {
|
|||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadMailboxes()
|
||||
onMounted(async () => {
|
||||
await loadMailboxes()
|
||||
|
||||
const mailboxParam = route.query.mailbox as string | undefined
|
||||
if (mailboxParam) {
|
||||
const found = store.mailboxes.find((b: any) => b.address === mailboxParam)
|
||||
if (found) store.currentMailbox = mailboxParam
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('message', (ev: MessageEvent) => {
|
||||
if (ev.data?.type === 'NOTIFICATION_CLICK' && ev.data.mailbox) {
|
||||
store.currentMailbox = ev.data.mailbox
|
||||
}
|
||||
if (ev.data?.type === 'SET_BADGE') {
|
||||
const count: number = ev.data.count ?? 0
|
||||
if ('setAppBadge' in navigator) {
|
||||
if (count > 0) {
|
||||
navigator.setAppBadge(count).catch(() => {})
|
||||
} else {
|
||||
navigator.clearAppBadge().catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => store.currentMailbox, async (addr) => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
|
||||
import { onMounted, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useMail } from '../composables/useMail';
|
||||
import { useMailPolling } from '../composables/useMailPolling';
|
||||
import { mailApi } from '../api/mail';
|
||||
import MailLayout from '../components/mail/MailLayout.vue';
|
||||
const route = useRoute();
|
||||
const { store, loadMailboxes } = useMail();
|
||||
useMailPolling();
|
||||
const loadMessages = async (addr, folder) => {
|
||||
if (!addr || !folder)
|
||||
return;
|
||||
|
|
@ -17,8 +21,21 @@ const loadMessages = async (addr, folder) => {
|
|||
console.error('Failed to load messages', e);
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
loadMailboxes();
|
||||
onMounted(async () => {
|
||||
await loadMailboxes();
|
||||
const mailboxParam = route.query.mailbox;
|
||||
if (mailboxParam) {
|
||||
const found = store.mailboxes.find((b) => b.address === mailboxParam);
|
||||
if (found)
|
||||
store.currentMailbox = mailboxParam;
|
||||
}
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('message', (ev) => {
|
||||
if (ev.data?.type === 'NOTIFICATION_CLICK' && ev.data.mailbox) {
|
||||
store.currentMailbox = ev.data.mailbox;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
watch(() => store.currentMailbox, async (addr) => {
|
||||
if (!addr)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"MailView.vue.js","sourceRoot":"","sources":["MailView.vue"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"MailView.vue.js","sourceRoot":"","sources":["MailView.vue"],"names":[],"mappings":"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"}
|
||||
|
|
@ -1,9 +1,130 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useInstallPrompt } from '@/composables/useInstallPrompt'
|
||||
import { useNotificationsStore } from '@/stores/notifications'
|
||||
import { usePushSubscription } from '@/composables/usePushSubscription'
|
||||
import { useMailStore } from '@/stores/mail'
|
||||
import { mailApi } from '@/api/mail'
|
||||
|
||||
const { canInstall, promptInstall } = useInstallPrompt()
|
||||
const notificationsStore = useNotificationsStore()
|
||||
const push = usePushSubscription()
|
||||
const mailStore = useMailStore()
|
||||
|
||||
const notificationPreview = ref(true)
|
||||
const previewLoading = ref(false)
|
||||
|
||||
watch(
|
||||
() => mailStore.currentMailbox,
|
||||
async (address) => {
|
||||
if (!address) return
|
||||
try {
|
||||
const res = await mailApi.getMailboxPreferences(address)
|
||||
notificationPreview.value = res.data.notification_preview
|
||||
} catch { /* ignore */ }
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function togglePreview() {
|
||||
if (!mailStore.currentMailbox || previewLoading.value) return
|
||||
previewLoading.value = true
|
||||
const next = !notificationPreview.value
|
||||
try {
|
||||
await mailApi.updateMailboxPreferences(mailStore.currentMailbox, { notification_preview: next })
|
||||
notificationPreview.value = next
|
||||
} catch { /* ignore */ } finally {
|
||||
previewLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-8">
|
||||
<h1 class="text-2xl font-semibold mb-4">Settings</h1>
|
||||
<p class="text-muted-foreground">Settings are managed in DockFlare Master.</p>
|
||||
<div class="p-8 max-w-2xl">
|
||||
<h1 class="text-2xl font-semibold mb-6">Settings</h1>
|
||||
|
||||
<div v-if="canInstall" class="mb-4 rounded-lg border p-4 space-y-3">
|
||||
<div>
|
||||
<h2 class="text-sm font-medium">Install App</h2>
|
||||
<p class="text-sm text-muted-foreground mt-0.5">
|
||||
Install DockFlare Mail as a desktop app for faster access and desktop notifications.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
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"
|
||||
@click="promptInstall"
|
||||
>
|
||||
Install
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 rounded-lg border p-4 space-y-3">
|
||||
<div>
|
||||
<h2 class="text-sm font-medium">Notifications</h2>
|
||||
<p class="text-sm text-muted-foreground mt-0.5">
|
||||
Get notified when new mail arrives, even when the app is closed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template v-if="notificationsStore.isDenied">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Notifications are blocked. Enable them in your browser or OS settings.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="!notificationsStore.isGranted">
|
||||
<button
|
||||
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"
|
||||
@click="notificationsStore.requestPermission"
|
||||
>
|
||||
Enable Notifications
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<p class="text-sm text-muted-foreground">Permission granted.</p>
|
||||
<div v-if="push.isSupported && mailStore.currentMailbox" class="flex items-center gap-3">
|
||||
<span class="text-sm">Background push for {{ mailStore.currentMailbox }}</span>
|
||||
<button
|
||||
v-if="!push.isSubscribed.value"
|
||||
:disabled="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"
|
||||
@click="push.subscribe(mailStore.currentMailbox)"
|
||||
>
|
||||
Enable
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
:disabled="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"
|
||||
@click="push.unsubscribe()"
|
||||
>
|
||||
Disable
|
||||
</button>
|
||||
</div>
|
||||
<p v-else-if="!push.isSupported" class="text-sm text-muted-foreground">
|
||||
Background push is not supported in this browser.
|
||||
</p>
|
||||
|
||||
<div v-if="mailStore.currentMailbox" class="flex items-center gap-3 pt-1">
|
||||
<span class="text-sm">Show subject & sender in notifications</span>
|
||||
<button
|
||||
:disabled="previewLoading"
|
||||
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 disabled:opacity-50"
|
||||
:class="notificationPreview ? 'bg-primary' : 'bg-muted'"
|
||||
role="switch"
|
||||
:aria-checked="notificationPreview"
|
||||
@click="togglePreview"
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow ring-0 transition-transform duration-200"
|
||||
:class="notificationPreview ? 'translate-x-5' : 'translate-x-0'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-muted-foreground">Additional settings are managed in DockFlare Master.</p>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,26 +1,211 @@
|
|||
/// <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();
|
||||
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
|
||||
const __VLS_ctx = {};
|
||||
let __VLS_components;
|
||||
let __VLS_directives;
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
|
||||
...{ class: "p-8" },
|
||||
...{ class: "p-8 max-w-2xl" },
|
||||
});
|
||||
__VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({
|
||||
...{ class: "text-2xl font-semibold mb-4" },
|
||||
...{ 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-muted-foreground" },
|
||||
...{ 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 {__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 {};
|
||||
return {
|
||||
canInstall: canInstall,
|
||||
promptInstall: promptInstall,
|
||||
notificationsStore: notificationsStore,
|
||||
push: push,
|
||||
mailStore: mailStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
export default (await import('vue')).defineComponent({
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -12,11 +12,12 @@
|
|||
"esModuleInterop": true,
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"types": ["vite/client"],
|
||||
"types": ["vite/client", "vite-plugin-pwa/client"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"exclude": ["src/sw.ts"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
@ -1,12 +1,49 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
plugins: [
|
||||
vue(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
strategies: 'injectManifest',
|
||||
srcDir: 'src',
|
||||
filename: 'sw.ts',
|
||||
manifest: {
|
||||
name: 'DockFlare Mail',
|
||||
short_name: 'Mail',
|
||||
display: 'standalone',
|
||||
display_override: ['window-controls-overlay', 'standalone'],
|
||||
start_url: '/',
|
||||
scope: '/',
|
||||
theme_color: '#0f172a',
|
||||
background_color: '#ffffff',
|
||||
icons: [
|
||||
{
|
||||
src: '/favicon/android-chrome-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: '/favicon/android-chrome-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any maskable'
|
||||
},
|
||||
{
|
||||
src: '/favicon/apple-touch-icon.png',
|
||||
sizes: '180x180',
|
||||
type: 'image/png'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue