mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-02-14 17:19:35 +01:00
Compare commits
3 commits
633d9c9b29
...
cc4e3ddfbd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc4e3ddfbd | ||
|
|
12069300d0 | ||
|
|
5226cb3c6b |
10 changed files with 209 additions and 21 deletions
10
app.js
10
app.js
|
|
@ -46,14 +46,20 @@ const mailProcessingService = new MailProcessingService(
|
||||||
)
|
)
|
||||||
debug('Mail processing service initialized')
|
debug('Mail processing service initialized')
|
||||||
|
|
||||||
|
// Track IMAP initialization state
|
||||||
|
let isImapReady = false
|
||||||
|
app.set('isImapReady', false)
|
||||||
|
|
||||||
// Put everything together:
|
// Put everything together:
|
||||||
imapService.on(ImapService.EVENT_NEW_MAIL, mail =>
|
imapService.on(ImapService.EVENT_NEW_MAIL, mail =>
|
||||||
mailProcessingService.onNewMail(mail)
|
mailProcessingService.onNewMail(mail)
|
||||||
)
|
)
|
||||||
debug('Bound IMAP new mail event handler')
|
debug('Bound IMAP new mail event handler')
|
||||||
imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, () =>
|
imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, () => {
|
||||||
mailProcessingService.onInitialLoadDone()
|
mailProcessingService.onInitialLoadDone()
|
||||||
)
|
isImapReady = true
|
||||||
|
app.set('isImapReady', true)
|
||||||
|
})
|
||||||
debug('Bound IMAP initial load done event handler')
|
debug('Bound IMAP initial load done event handler')
|
||||||
imapService.on(ImapService.EVENT_DELETED_MAIL, mail =>
|
imapService.on(ImapService.EVENT_DELETED_MAIL, mail =>
|
||||||
mailProcessingService.onMailDeleted(mail)
|
mailProcessingService.onMailDeleted(mail)
|
||||||
|
|
|
||||||
|
|
@ -281,11 +281,41 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initHamburgerMenu() {
|
||||||
|
const actionLinks = document.querySelector('.action-links');
|
||||||
|
if (!actionLinks) return;
|
||||||
|
|
||||||
|
// Create hamburger button
|
||||||
|
const hamburger = document.createElement('button');
|
||||||
|
hamburger.className = 'hamburger-menu';
|
||||||
|
hamburger.setAttribute('aria-label', 'Toggle menu');
|
||||||
|
hamburger.innerHTML = '<span></span><span></span><span></span>';
|
||||||
|
|
||||||
|
// Insert as first child
|
||||||
|
actionLinks.insertBefore(hamburger, actionLinks.firstChild);
|
||||||
|
actionLinks.classList.add('mobile-hidden');
|
||||||
|
|
||||||
|
hamburger.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
actionLinks.classList.toggle('mobile-hidden');
|
||||||
|
actionLinks.classList.toggle('mobile-open');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close menu when clicking outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (actionLinks.classList.contains('mobile-open') && !actionLinks.contains(e.target)) {
|
||||||
|
actionLinks.classList.remove('mobile-open');
|
||||||
|
actionLinks.classList.add('mobile-hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Expose utilities and run them
|
// Expose utilities and run them
|
||||||
window.utils = { formatEmailDates, formatMailDate, initLockModals, initCopyAddress, initExpiryTimers, initQrModal };
|
window.utils = { formatEmailDates, formatMailDate, initLockModals, initCopyAddress, initExpiryTimers, initQrModal, initHamburgerMenu };
|
||||||
formatEmailDates();
|
formatEmailDates();
|
||||||
formatMailDate();
|
formatMailDate();
|
||||||
initLockModals();
|
initLockModals();
|
||||||
initCopyAddress();
|
initCopyAddress();
|
||||||
initQrModal();
|
initQrModal();
|
||||||
|
initHamburgerMenu();
|
||||||
});
|
});
|
||||||
|
|
@ -158,6 +158,39 @@ text-muted {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
/* ensures they stay in one line */
|
/* ensures they stay in one line */
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-menu {
|
||||||
|
display: none;
|
||||||
|
background: var(--overlay-purple-20);
|
||||||
|
border: 1px solid var(--overlay-purple-30);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-accent-purple-light);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-menu span {
|
||||||
|
width: 24px;
|
||||||
|
height: 2px;
|
||||||
|
background: currentColor;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburger-menu:hover {
|
||||||
|
background: var(--overlay-purple-30);
|
||||||
|
border-color: var(--overlay-purple-40);
|
||||||
|
box-shadow: 0 4px 15px var(--overlay-purple-25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-links a {
|
.action-links a {
|
||||||
|
|
@ -341,9 +374,11 @@ label {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inbox-title {
|
.inbox-title {
|
||||||
|
|
@ -735,7 +770,9 @@ label {
|
||||||
display: none;
|
display: none;
|
||||||
color: var(--color-accent-purple-alt);
|
color: var(--color-accent-purple-alt);
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
margin-left: 10px;
|
position: absolute;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-expiry .expiry-timer[style*="color: #b00"] {
|
.email-expiry .expiry-timer[style*="color: #b00"] {
|
||||||
|
|
@ -835,6 +872,7 @@ label {
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-modal-content h3 {
|
.qr-modal-content h3 {
|
||||||
|
margin-left: 16.516px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -853,4 +891,82 @@ label {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Loading Page */
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 80vh;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-title {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-message {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-submessage {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.loading-page .logo {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Responsive Styles */
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.action-links {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.hamburger-menu {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.action-links.mobile-hidden>a,
|
||||||
|
.action-links.mobile-hidden>button:not(.hamburger-menu) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.action-links.mobile-open {
|
||||||
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 20px;
|
||||||
|
background: var(--color-bg-dark);
|
||||||
|
border: 1px solid var(--overlay-purple-30);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 15px;
|
||||||
|
box-shadow: 0 10px 40px var(--overlay-black-40);
|
||||||
|
z-index: 1000;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
.action-links.mobile-open>.hamburger-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.action-links.mobile-open>a,
|
||||||
|
.action-links.mobile-open>button:not(.hamburger-menu) {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.qr-icon-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<div class="action-links">
|
<div class="action-links">
|
||||||
{% if showUnlockButton %}
|
{% if showUnlockButton %}
|
||||||
<a href="#" id="unlockBtn">Unlock</a>
|
<a href="#" id="unlockBtn" aria-label="Unlock inbox">Unlock</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="/">Logout</a>
|
<a href="/" aria-label="Return to home">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,18 @@
|
||||||
<div class="action-links">
|
<div class="action-links">
|
||||||
{% if lockEnabled %}
|
{% if lockEnabled %}
|
||||||
{% if isLocked and hasAccess %}
|
{% if isLocked and hasAccess %}
|
||||||
<a href="#" id="removeLockBtn">Remove Lock</a>
|
<a href="#" id="removeLockBtn" aria-label="Remove password lock">Remove Lock</a>
|
||||||
{% elseif isLocked %}
|
{% elseif isLocked %}
|
||||||
<a href="#" id="unlockBtn">Unlock</a>
|
<a href="#" id="unlockBtn" aria-label="Unlock inbox">Unlock</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="#" id="lockBtn">Protect Inbox</a>
|
<a href="#" id="lockBtn" aria-label="Protect inbox with password">Protect Inbox</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="/inbox/{{ address }}/delete-all">Wipe Inbox</a>
|
<a href="/inbox/{{ address }}/delete-all" aria-label="Delete all emails">Wipe Inbox</a>
|
||||||
{% if lockEnabled and hasAccess %}
|
{% if lockEnabled and hasAccess %}
|
||||||
<a href="/lock/logout">Logout</a>
|
<a href="/lock/logout" aria-label="Logout">Logout</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/logout">Logout</a>
|
<a href="/logout" aria-label="Logout">Logout</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -75,11 +75,11 @@
|
||||||
<script src="/javascripts/notifications.js" defer="true"></script>
|
<script src="/javascripts/notifications.js" defer="true"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body{% if bodyClass %} class="{{ bodyClass }}"{% endif %}>
|
||||||
<main>
|
<main>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<a href="/">
|
<a href="/" aria-label="48hr.email home">
|
||||||
<img src="/images/logo.png" class="logo" style="max-width: 75px">
|
<img src="/images/logo.png" class="logo" alt="48hr.email logo" style="max-width: 75px">
|
||||||
</a>
|
</a>
|
||||||
{% block header %}{% endblock %}
|
{% block header %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
27
infrastructure/web/views/loading.twig
Normal file
27
infrastructure/web/views/loading.twig
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends "layout.twig" %}
|
||||||
|
|
||||||
|
{% set bodyClass = 'loading-page' %}
|
||||||
|
|
||||||
|
{% block header %}{% endblock %}
|
||||||
|
{% block footer %}{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="loading-container">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<svg width="80" height="80" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="50" cy="50" r="40" stroke="var(--color-accent-purple-light)" stroke-width="8" fill="none" stroke-dasharray="63 188" stroke-linecap="round">
|
||||||
|
<animateTransform attributeName="transform" type="rotate" from="0 50 50" to="360 50 50" dur="1.5s" repeatCount="indefinite"/>
|
||||||
|
</circle>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="loading-title">Loading Mail Service</h2>
|
||||||
|
<p class="loading-message">Connecting to IMAP server and loading messages...</p>
|
||||||
|
<p class="loading-submessage">This may take a few moments on first startup</p>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// Auto-refresh every 2 seconds to check if IMAP is ready
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 2000);
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<div class="action-links">
|
<div class="action-links">
|
||||||
<a href="/inbox/{{ address }}">← Return to inbox</a>
|
<a href="/inbox/{{ address }}" aria-label="Return to inbox">← Return to inbox</a>
|
||||||
<a href="/inbox/{{ address }}/{{ uid }}/delete">Delete Email</a>
|
<a href="/inbox/{{ address }}/{{ uid }}/delete" aria-label="Delete this email">Delete Email</a>
|
||||||
<a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank">View Raw</a>
|
<a href="/inbox/{{ address }}/{{ uid }}/raw" target="_blank" aria-label="View raw email">View Raw</a>
|
||||||
{% if lockEnabled and isLocked and hasAccess %}
|
{% if lockEnabled and isLocked and hasAccess %}
|
||||||
<a href="/lock/logout">Logout</a>
|
<a href="/lock/logout" aria-label="Logout">Logout</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/logout">Logout</a>
|
<a href="/logout" aria-label="Logout">Logout</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,15 @@ app.use(
|
||||||
)
|
)
|
||||||
Twig.extendFilter('sanitizeHtml', sanitizeHtmlTwigFilter)
|
Twig.extendFilter('sanitizeHtml', sanitizeHtmlTwigFilter)
|
||||||
|
|
||||||
|
// Middleware to show loading page until IMAP is ready
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const isImapReady = req.app.get('isImapReady')
|
||||||
|
if (!isImapReady && !req.path.startsWith('/images') && !req.path.startsWith('/javascripts') && !req.path.startsWith('/stylesheets') && !req.path.startsWith('/dependencies')) {
|
||||||
|
return res.render('loading')
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
app.use('/', loginRouter)
|
app.use('/', loginRouter)
|
||||||
app.use('/inbox', inboxRouter)
|
app.use('/inbox', inboxRouter)
|
||||||
app.use('/error', errorRouter)
|
app.use('/error', errorRouter)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "48hr.email",
|
"name": "48hr.email",
|
||||||
"version": "1.7.2",
|
"version": "1.7.3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "48hr.email is your favorite open-source tempmail client.",
|
"description": "48hr.email is your favorite open-source tempmail client.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue