updated layout for webmail and added http dns letsencrypt
This commit is contained in:
@@ -8,34 +8,71 @@
|
||||
<link href="/webmail/static/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/webmail/static/vendor/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background-color: #1a1a1a; color: #e0e0e0; }
|
||||
html, body { background-color: #1a1a1a; color: #e0e0e0; height: 100%; }
|
||||
.card { background-color: #2d2d2d; border: 1px solid #404040; }
|
||||
.table-dark { --bs-table-bg: #2d2d2d; --bs-table-border-color: #404040; }
|
||||
.folder-link.active { background-color: #0d6efd; color: #fff !important; }
|
||||
.msg-unread { font-weight: 600; }
|
||||
.msg-row { cursor: grab; }
|
||||
.msg-row.dragging { opacity: 0.4; }
|
||||
.folder-link.drop-hover { background-color: #0d6efd; color: #fff !important; outline: 2px dashed #6ea8fe; outline-offset: -2px; }
|
||||
.folder-unread-badge { font-size: .7rem; }
|
||||
.msg-row-older { display: none; }
|
||||
.msg-group-toggle { cursor: pointer; }
|
||||
/* A search result can span multiple folders, and the bulk-action endpoint is
|
||||
scoped to one folder path — rather than a bulk action silently no-oping on
|
||||
every row from a different folder, selection/bulk actions are just not
|
||||
offered while searching (per-row actions in the reading pane still work). */
|
||||
.search-mode .msg-check, .search-mode #selectAllCheck, .search-mode .bulk-btn { display: none; }
|
||||
|
||||
/* Outlook-style three-pane shell: fixed-width sidebar + fixed-width list +
|
||||
flexible reading pane, instead of a responsive 12-column grid — this is
|
||||
deliberately a fixed desktop layout to match the reference, not a
|
||||
mobile-first one. */
|
||||
.mail-shell { display: flex; align-items: stretch; height: calc(100vh - 56px); overflow: hidden; }
|
||||
.mail-sidebar { width: 230px; flex: 0 0 auto; overflow-y: auto; border-right: 1px solid #404040; padding: .75rem; }
|
||||
.mail-list-pane { width: 380px; flex: 0 0 auto; overflow-y: auto; border-right: 1px solid #404040; display: flex; flex-direction: column; }
|
||||
.mail-reading-pane { flex: 1 1 auto; overflow-y: auto; padding: 1.5rem; min-width: 0; }
|
||||
.mail-toolbar { flex: 0 0 auto; padding: .5rem .75rem; border-bottom: 1px solid #404040; display: flex; align-items: center; gap: .35rem; flex-wrap: wrap; }
|
||||
.mail-list-scroll { flex: 1 1 auto; overflow-y: auto; }
|
||||
.mail-list-header { padding: .35rem .75rem; font-size: .75rem; text-transform: uppercase; color: #8a8a8a; display: flex; justify-content: space-between; }
|
||||
|
||||
.msg-item { display: flex; align-items: flex-start; gap: .6rem; padding: .55rem .75rem; border-bottom: 1px solid #333; cursor: pointer; }
|
||||
.msg-item:hover { background-color: #262626; }
|
||||
.msg-item.active { background-color: #0d3860; }
|
||||
.msg-item.unread .msg-subject { font-weight: 700; color: #fff; }
|
||||
.msg-item.unread .msg-from { font-weight: 700; color: #fff; }
|
||||
.msg-check { margin-top: .35rem; flex: 0 0 auto; }
|
||||
.msg-avatar { width: 34px; height: 34px; border-radius: 50%; background: #495057; color: #fff; display: flex; align-items: center; justify-content: center; font-size: .85rem; font-weight: 600; flex: 0 0 auto; }
|
||||
.msg-item.unread .msg-avatar { background: #0d6efd; }
|
||||
.msg-main { min-width: 0; flex: 1 1 auto; }
|
||||
.msg-row1 { display: flex; justify-content: space-between; gap: .5rem; }
|
||||
.msg-from { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.msg-date { flex: 0 0 auto; font-size: .75rem; color: #8a8a8a; }
|
||||
.msg-row2 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .85rem; color: #adb5bd; }
|
||||
.msg-subject { color: #e0e0e0; }
|
||||
|
||||
.msg-body-html { background-color: #fff; color: #000; border-radius: 6px; padding: 1rem; overflow-x: auto; }
|
||||
.msg-body-text { white-space: pre-wrap; word-break: break-word; }
|
||||
#readingPaneBody .pane-toolbar { border-bottom: 1px solid #404040; padding-bottom: .75rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{template "csrf_script" .}}
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand mb-0 h1"><i class="bi bi-inbox-fill me-2"></i>Webmail <small class="text-muted">{{.mailbox.Email}}</small></span>
|
||||
<div class="navbar-nav flex-row gap-2 ms-auto">
|
||||
<a href="/webmail/mail/INBOX" class="btn btn-outline-light btn-sm"><i class="bi bi-envelope me-1"></i>Mail</a>
|
||||
<button type="button" onclick="openCompose('/webmail/mail/compose')" class="btn btn-outline-light btn-sm"><i class="bi bi-pencil-square me-1"></i>Compose</button>
|
||||
<a href="/webmail/rules" class="btn btn-outline-light btn-sm"><i class="bi bi-funnel me-1"></i>Rules</a>
|
||||
<a href="/webmail/certs" class="btn btn-outline-light btn-sm"><i class="bi bi-shield-lock me-1"></i>Certs</a>
|
||||
<a href="/webmail/account" class="btn btn-outline-light btn-sm"><i class="bi bi-gear me-1"></i>Account</a>
|
||||
<form method="post" action="/webmail/logout" class="d-inline">
|
||||
<button type="submit" class="btn btn-outline-light btn-sm"><i class="bi bi-box-arrow-right me-1"></i>Sign out</button>
|
||||
</form>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark px-3" style="height: 56px;">
|
||||
<span class="navbar-brand mb-0 h1"><i class="bi bi-envelope-fill me-2"></i>Webmail <small class="text-muted">{{.mailbox.Email}}</small></span>
|
||||
<form method="get" action="/webmail/mail/search" class="mx-auto" style="width: 360px;">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text bg-body-secondary border-0"><i class="bi bi-search"></i></span>
|
||||
<input type="search" name="q" id="mailSearchInput" class="form-control" placeholder="Search all mail" value="{{.search_query}}">
|
||||
</div>
|
||||
</form>
|
||||
<div class="navbar-nav flex-row gap-2 ms-auto">
|
||||
<button type="button" onclick="openCompose('/webmail/mail/compose')" class="btn btn-primary btn-sm"><i class="bi bi-pencil-square me-1"></i>Compose</button>
|
||||
<a href="/webmail/rules" class="btn btn-outline-light btn-sm" title="Rules"><i class="bi bi-funnel"></i></a>
|
||||
<a href="/webmail/certs" class="btn btn-outline-light btn-sm" title="Certs"><i class="bi bi-shield-lock"></i></a>
|
||||
<a href="/webmail/account" class="btn btn-outline-light btn-sm" title="Account"><i class="bi bi-gear"></i></a>
|
||||
<form method="post" action="/webmail/logout" class="d-inline">
|
||||
<button type="submit" class="btn btn-outline-light btn-sm" title="Sign out"><i class="bi bi-box-arrow-right"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -53,117 +90,123 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="container-fluid pb-5">
|
||||
<div class="row">
|
||||
<div class="col-lg-2 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-body p-2">
|
||||
<form method="get" action="/webmail/mail/search" class="mb-2">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="search" name="q" id="mailSearchInput" class="form-control" placeholder="Search all mail" value="{{.search_query}}">
|
||||
<button type="submit" class="btn btn-outline-light"><i class="bi bi-search"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="list-group list-group-flush">
|
||||
{{$active := .active_folder}}
|
||||
{{$unread := .unread_counts}}
|
||||
{{range .folders}}
|
||||
<div class="d-flex align-items-center folder-row">
|
||||
<a href="/webmail/mail/{{.}}" data-folder="{{.}}" class="list-group-item list-group-item-action bg-transparent text-white folder-link flex-grow-1 d-flex justify-content-between align-items-center {{if eq . $active}}active{{end}}">
|
||||
<span><i class="bi bi-folder2 me-1"></i>{{.}}</span>
|
||||
{{$n := index $unread .}}
|
||||
{{if $n}}<span class="badge bg-primary rounded-pill folder-unread-badge">{{$n}}</span>{{end}}
|
||||
</a>
|
||||
{{if not (isStandardFolder .)}}
|
||||
<form method="post" action="/webmail/mail/folders/{{.}}/remove" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger border-0" title="Remove folder" data-confirm="Remove folder "{{.}}"? Any mail in it moves to INBOX."><i class="bi bi-x-lg"></i></button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<hr class="my-2">
|
||||
<form method="post" action="/webmail/mail/folders/add" class="d-flex gap-1">
|
||||
<input type="text" class="form-control form-control-sm" name="name" placeholder="New folder" maxlength="60" required>
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary" title="Create folder"><i class="bi bi-plus-lg"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="mail-shell">
|
||||
<div class="mail-sidebar" id="folderSidebarCol">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="text-muted small text-uppercase">Folders</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary border-0 py-0" id="sidebarCollapseBtn" title="Hide folder list"><i class="bi bi-chevron-bar-left"></i></button>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
{{$active := .active_folder}}
|
||||
{{$unread := .unread_counts}}
|
||||
{{$counts := .folder_counts}}
|
||||
{{range .folders}}
|
||||
<div class="d-flex align-items-center folder-row">
|
||||
<a href="/webmail/mail/{{.}}" data-folder="{{.}}" class="list-group-item list-group-item-action bg-transparent text-white folder-link flex-grow-1 d-flex justify-content-between align-items-center {{if eq . $active}}active{{end}}">
|
||||
<span><i class="bi bi-folder2 me-1"></i>{{.}}</span>
|
||||
{{$n := index $unread .}}
|
||||
{{$total := index $counts .}}
|
||||
{{if $total}}
|
||||
<span class="badge {{if $n}}bg-primary{{else}}bg-secondary{{end}} rounded-pill folder-unread-badge" title="{{$total}} total{{if $n}}, {{$n}} unread{{end}}">{{$total}}{{if $n}} / <strong>{{$n}}</strong>{{end}}</span>
|
||||
{{end}}
|
||||
</a>
|
||||
{{if not (isStandardFolder .)}}
|
||||
<form method="post" action="/webmail/mail/folders/{{.}}/remove" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger border-0" title="Remove folder" data-confirm="Remove folder "{{.}}"? Any mail in it moves to INBOX."><i class="bi bi-x-lg"></i></button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<hr class="my-2">
|
||||
<form method="post" action="/webmail/mail/folders/add" class="d-flex gap-1">
|
||||
<input type="text" class="form-control form-control-sm" name="name" placeholder="New folder" maxlength="60" required>
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary" title="Create folder"><i class="bi bi-plus-lg"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="mail-list-pane{{if .search_query}} search-mode{{end}}" id="messageListCol">
|
||||
<div class="mail-toolbar">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary border-0 py-0 d-none" id="sidebarShowBtn" title="Show folder list"><i class="bi bi-chevron-bar-right"></i></button>
|
||||
<input type="checkbox" class="form-check-input" id="selectAllCheck" title="Select all">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-outline-secondary bulk-btn" data-action="delete" disabled title="Delete"><i class="bi bi-trash"></i></button>
|
||||
<button type="button" class="btn btn-outline-secondary bulk-btn" data-action="read" disabled title="Mark as read"><i class="bi bi-envelope-open"></i></button>
|
||||
<button type="button" class="btn btn-outline-secondary bulk-btn" data-action="unread" disabled title="Mark as unread"><i class="bi bi-envelope"></i></button>
|
||||
</div>
|
||||
{{if not .search_query}}
|
||||
<select class="form-select form-select-sm bulk-move-select" id="bulkMoveSelect" disabled style="width: auto;" title="Move selected to…">
|
||||
<option value="">Move to…</option>
|
||||
{{$folder := .active_folder}}
|
||||
{{range .folders}}{{if ne . $folder}}<option value="{{.}}">{{.}}</option>{{end}}{{end}}
|
||||
</select>
|
||||
{{end}}
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary border-0" onclick="location.reload()" title="Refresh"><i class="bi bi-arrow-clockwise"></i></button>
|
||||
<div class="ms-auto d-flex align-items-center gap-1">
|
||||
{{if not .search_query}}
|
||||
<a href="{{.sort_from_href}}" class="btn btn-sm btn-outline-secondary border-0 py-0" title="Sort by sender"><i class="bi bi-person{{if eq .sort_by "from"}}-fill{{end}}"></i>{{if eq .sort_by "from"}} <i class="bi bi-caret-{{if eq .sort_dir "asc"}}up{{else}}down{{end}}-fill"></i>{{end}}</a>
|
||||
<a href="{{.sort_date_href}}" class="btn btn-sm btn-outline-secondary border-0 py-0" title="Sort by date"><i class="bi bi-calendar3{{if ne .sort_by "from"}}-fill{{end}}"></i>{{if ne .sort_by "from"}} <i class="bi bi-caret-{{if eq .sort_dir "asc"}}up{{else}}down{{end}}-fill"></i>{{end}}</a>
|
||||
<a href="{{.unread_only_href}}" class="btn btn-sm border-0 py-0 {{if .unread_only}}btn-primary{{else}}btn-outline-secondary{{end}}" title="Unread only"><i class="bi bi-envelope-fill"></i></a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-10 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
{{if .search_query}}<i class="bi bi-search me-2"></i>Search results for “{{.search_query}}”
|
||||
{{else}}<i class="bi bi-folder2-open me-2"></i>{{.active_folder}}{{end}}
|
||||
</h5>
|
||||
<small class="text-muted">{{.total}} message{{if ne .total 1}}s{{end}}</small>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{{if .messages}}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
{{if not .search_query}}<th>{{if eq .active_folder "Sent"}}To{{else}}From{{end}}</th>{{else}}<th>From / To</th>{{end}}
|
||||
<th>Subject</th>
|
||||
{{if .search_query}}<th>Folder</th>{{end}}
|
||||
<th>Date</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{$folders := .folders}}
|
||||
{{$showFolderCol := .search_query}}
|
||||
{{range .messages}}
|
||||
{{$rowHref := printf "/webmail/mail/%s/%d" .Folder .ID}}
|
||||
{{if eq .Folder "Drafts"}}{{$rowHref = printf "/webmail/mail/compose?draft=%d&folder=Drafts" .ID}}{{end}}
|
||||
<tr class="{{if .Unread}}msg-unread{{end}} msg-row{{if .Collapsed}} msg-row-older{{end}}" draggable="true" data-uid="{{.ID}}" data-folder="{{.Folder}}">
|
||||
<td><a class="text-reset text-decoration-none" href="{{$rowHref}}">{{if eq .Folder "Sent"}}{{if .CachedTo}}{{.CachedTo}}{{else}}(no recipient){{end}}{{else}}{{.CachedFrom}}{{end}}</a></td>
|
||||
<td>
|
||||
<a class="text-reset text-decoration-none" href="{{$rowHref}}">{{if .CachedSubject}}{{.CachedSubject}}{{else}}<span class="text-muted">(no subject)</span>{{end}}</a>
|
||||
{{if gt .GroupExtra 0}}<span class="badge bg-secondary msg-group-toggle" data-group-toggle="{{.ID}}">+{{.GroupExtra}} more</span>{{end}}
|
||||
</td>
|
||||
{{if $showFolderCol}}<td><small class="text-muted">{{.Folder}}</small></td>{{end}}
|
||||
<td><small class="text-muted">{{strftime "%Y-%m-%d %H:%M" .InternalDate}}</small></td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<form method="post" action="/webmail/mail/{{.Folder}}/{{.ID}}/move" class="d-inline-flex">
|
||||
<select name="target_folder" class="form-select form-select-sm" style="width: auto;" onchange="this.form.submit()">
|
||||
<option value="">Move to…</option>
|
||||
{{$rowFolder := .Folder}}
|
||||
{{range $folders}}{{if ne . $rowFolder}}<option value="{{.}}">{{.}}</option>{{end}}{{end}}
|
||||
</select>
|
||||
</form>
|
||||
<form method="post" action="/webmail/mail/{{.Folder}}/{{.ID}}/delete" class="d-inline">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm" title="{{if eq .Folder "Trash"}}Delete permanently{{else}}Move to Trash{{end}}" data-confirm="{{if eq .Folder "Trash"}}Permanently delete this message? This cannot be undone.{{else}}Move this message to Trash?{{end}}"><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mail-list-header">
|
||||
<span>{{if .search_query}}Search: “{{.search_query}}”{{else}}{{.active_folder}}{{end}}</span>
|
||||
<span>{{.total}} message{{if ne .total 1}}s{{end}}</span>
|
||||
</div>
|
||||
<div class="mail-list-scroll" id="mailListScroll">
|
||||
{{if .messages}}
|
||||
{{range .messages}}
|
||||
{{$rowHref := printf "/webmail/mail/%s/%d" .Folder .ID}}
|
||||
{{$paneHref := printf "/webmail/mail/%s/%d/pane" .Folder .ID}}
|
||||
{{$isDraft := eq .Folder "Drafts"}}
|
||||
{{if $isDraft}}{{$rowHref = printf "/webmail/mail/compose?draft=%d&folder=Drafts" .ID}}{{end}}
|
||||
{{$displayName := senderName .CachedFrom}}
|
||||
{{if eq .Folder "Sent"}}{{if .CachedTo}}{{$displayName = senderName .CachedTo}}{{else}}{{$displayName = "(no recipient)"}}{{end}}{{end}}
|
||||
<div class="msg-item {{if .Unread}}unread{{end}}{{if .Collapsed}} msg-row-older{{end}} msg-row" draggable="true" data-uid="{{.ID}}" data-folder="{{.Folder}}" data-href="{{$rowHref}}" data-pane-href="{{$paneHref}}" data-is-draft="{{$isDraft}}">
|
||||
<input type="checkbox" class="form-check-input msg-check" value="{{.ID}}" onclick="event.stopPropagation()">
|
||||
<div class="msg-avatar">{{initial $displayName}}</div>
|
||||
<div class="msg-main">
|
||||
<div class="msg-row1">
|
||||
<span class="msg-from" title="{{if eq .Folder "Sent"}}{{.CachedTo}}{{else}}{{.CachedFrom}}{{end}}">{{$displayName}}</span>
|
||||
<span class="msg-date">{{strftime "%Y-%m-%d %H:%M" .InternalDate}}</span>
|
||||
</div>
|
||||
<div class="msg-row2">
|
||||
<span class="msg-subject">{{if .CachedSubject}}{{.CachedSubject}}{{else}}(no subject){{end}}</span>
|
||||
{{if gt .GroupExtra 0}}<span class="badge bg-secondary msg-group-toggle" data-group-toggle="{{.ID}}">+{{.GroupExtra}} more</span>{{end}}
|
||||
{{if .CachedPreview}} – {{.CachedPreview}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{if or .has_prev .has_next}}
|
||||
<div class="d-flex justify-content-between p-3">
|
||||
{{if .has_prev}}<a href="?page={{sub .page 1}}" class="btn btn-outline-secondary btn-sm">« Newer</a>{{else}}<span></span>{{end}}
|
||||
{{if .has_next}}<a href="?page={{add .page 1}}" class="btn btn-outline-secondary btn-sm">Older »</a>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
|
||||
<h5 class="text-muted mt-3">No messages in {{.active_folder}}</h5>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-inbox text-muted" style="font-size: 3rem;"></i>
|
||||
<h6 class="text-muted mt-3">No messages</h6>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if or .has_prev .has_next}}
|
||||
<div class="d-flex justify-content-between p-2 border-top" style="border-color: #404040 !important;">
|
||||
{{if .has_prev}}<a href="{{.prev_href}}" class="btn btn-outline-secondary btn-sm">« Newer</a>{{else}}<span></span>{{end}}
|
||||
{{if .has_next}}<a href="{{.next_href}}" class="btn btn-outline-secondary btn-sm">Older »</a>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="mail-reading-pane" id="readingPaneBody">
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="bi bi-envelope-open" style="font-size: 3rem;"></i>
|
||||
<p class="mt-3">Select a message to read</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" id="bulkActionForm" class="d-none">
|
||||
<input type="hidden" name="action" id="bulkActionField">
|
||||
<input type="hidden" name="target_folder" id="bulkTargetFolderField">
|
||||
</form>
|
||||
|
||||
<div class="modal fade" id="confirmationModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
@@ -217,9 +260,9 @@
|
||||
});
|
||||
|
||||
// Drag a message row onto a folder in the sidebar to move it there — a
|
||||
// shortcut for the same "Move to..." dropdown every row already has. Each
|
||||
// row carries its OWN folder (data-folder) rather than assuming the page's
|
||||
// active folder, since a search result can span multiple folders.
|
||||
// shortcut for the toolbar's "Move to..." control. Each row carries its OWN
|
||||
// folder (data-folder) rather than assuming the page's active folder, since a
|
||||
// search result can span multiple folders.
|
||||
(function() {
|
||||
let draggedUID = null;
|
||||
let draggedFolder = null;
|
||||
@@ -251,6 +294,7 @@
|
||||
if (!draggedUID || !targetFolder || targetFolder === draggedFolder) return;
|
||||
const body = new URLSearchParams();
|
||||
body.set('target_folder', targetFolder);
|
||||
body.set('csrf_token', window.__csrfToken || '');
|
||||
await fetch(`/webmail/mail/${draggedFolder}/${draggedUID}/move`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
@@ -268,7 +312,7 @@
|
||||
badge.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
let sib = badge.closest('tr').nextElementSibling;
|
||||
let sib = badge.closest('.msg-item').nextElementSibling;
|
||||
while (sib && sib.classList.contains('msg-row-older')) {
|
||||
sib.style.display = '';
|
||||
sib = sib.nextElementSibling;
|
||||
@@ -276,6 +320,123 @@
|
||||
badge.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Folder sidebar collapse — a display preference remembered per-browser
|
||||
// (localStorage), not server state; default is pinned open (nothing stored
|
||||
// yet == not collapsed).
|
||||
(function() {
|
||||
const KEY = 'webmail_sidebar_collapsed';
|
||||
const sidebarCol = document.getElementById('folderSidebarCol');
|
||||
const showBtn = document.getElementById('sidebarShowBtn');
|
||||
function apply(collapsed) {
|
||||
sidebarCol.style.display = collapsed ? 'none' : '';
|
||||
showBtn.classList.toggle('d-none', !collapsed);
|
||||
}
|
||||
apply(localStorage.getItem(KEY) === '1');
|
||||
document.getElementById('sidebarCollapseBtn').addEventListener('click', function() {
|
||||
localStorage.setItem(KEY, '1');
|
||||
apply(true);
|
||||
});
|
||||
showBtn.addEventListener('click', function() {
|
||||
localStorage.setItem(KEY, '0');
|
||||
apply(false);
|
||||
});
|
||||
})();
|
||||
|
||||
// Reading pane: clicking a row loads the message via fetch instead of
|
||||
// navigating away, mirroring the admin dashboard's message-log modal. Drafts
|
||||
// still navigate to compose (there's nothing to "read"). The clicked row is
|
||||
// marked read optimistically client-side — the pane fetch itself is what
|
||||
// actually marks it read server-side (see webmailMessagePane).
|
||||
(function() {
|
||||
const paneBody = document.getElementById('readingPaneBody');
|
||||
document.querySelectorAll('.msg-item').forEach(function(row) {
|
||||
row.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.msg-group-toggle') || e.target.classList.contains('msg-check')) return;
|
||||
if (row.dataset.isDraft === 'true') { window.location.href = row.dataset.href; return; }
|
||||
document.querySelectorAll('.msg-item.active').forEach(function(r) { r.classList.remove('active'); });
|
||||
row.classList.add('active');
|
||||
row.classList.remove('unread');
|
||||
paneBody.innerHTML = '<div class="text-center text-muted py-5"><div class="spinner-border" role="status"></div></div>';
|
||||
fetch(row.dataset.paneHref)
|
||||
.then(function(r) { if (!r.ok) throw new Error(r.status); return r.text(); })
|
||||
.then(function(html) { paneBody.innerHTML = html; })
|
||||
.catch(function() { paneBody.innerHTML = '<p class="text-danger">Failed to load the message.</p>'; });
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
// Selection (checkboxes + select-all + Shift-click range) driving the bulk
|
||||
// toolbar buttons — enabled only once something's actually selected.
|
||||
(function() {
|
||||
const checks = Array.from(document.querySelectorAll('.msg-check'));
|
||||
const selectAll = document.getElementById('selectAllCheck');
|
||||
const bulkBtns = document.querySelectorAll('.bulk-btn');
|
||||
const moveSelect = document.getElementById('bulkMoveSelect');
|
||||
let lastCheckedIndex = null;
|
||||
|
||||
function updateToolbar() {
|
||||
const any = checks.some(function(c) { return c.checked; });
|
||||
bulkBtns.forEach(function(b) { b.disabled = !any; });
|
||||
if (moveSelect) moveSelect.disabled = !any;
|
||||
selectAll.checked = checks.length > 0 && checks.every(function(c) { return c.checked; });
|
||||
}
|
||||
|
||||
checks.forEach(function(cb, i) {
|
||||
cb.addEventListener('click', function(e) {
|
||||
if (e.shiftKey && lastCheckedIndex !== null) {
|
||||
const [from, to] = [lastCheckedIndex, i].sort(function(a, b) { return a - b; });
|
||||
for (let j = from; j <= to; j++) checks[j].checked = cb.checked;
|
||||
}
|
||||
lastCheckedIndex = i;
|
||||
updateToolbar();
|
||||
});
|
||||
});
|
||||
selectAll.addEventListener('change', function() {
|
||||
checks.forEach(function(c) { c.checked = selectAll.checked; });
|
||||
updateToolbar();
|
||||
});
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'a' && document.activeElement.tagName !== 'INPUT') {
|
||||
e.preventDefault();
|
||||
selectAll.checked = true;
|
||||
checks.forEach(function(c) { c.checked = true; });
|
||||
updateToolbar();
|
||||
}
|
||||
});
|
||||
|
||||
function selectedUIDs() { return checks.filter(function(c) { return c.checked; }).map(function(c) { return c.value; }); }
|
||||
function submitBulk(action, targetFolder) {
|
||||
const uids = selectedUIDs();
|
||||
if (uids.length === 0) return;
|
||||
const form = document.getElementById('bulkActionForm');
|
||||
form.action = '/webmail/mail/{{.active_folder}}/bulk';
|
||||
document.getElementById('bulkActionField').value = action;
|
||||
document.getElementById('bulkTargetFolderField').value = targetFolder || '';
|
||||
form.querySelectorAll('input[name="uid"]').forEach(function(el) { el.remove(); });
|
||||
uids.forEach(function(uid) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden'; input.name = 'uid'; input.value = uid;
|
||||
form.appendChild(input);
|
||||
});
|
||||
const csrf = document.createElement('input');
|
||||
csrf.type = 'hidden'; csrf.name = 'csrf_token'; csrf.value = window.__csrfToken || '';
|
||||
form.appendChild(csrf);
|
||||
form.submit();
|
||||
}
|
||||
document.querySelectorAll('.bulk-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', async function() {
|
||||
const action = btn.dataset.action;
|
||||
if (action === 'delete' && !(await showConfirmation('Move the selected message(s) to Trash?'))) return;
|
||||
submitBulk(action);
|
||||
});
|
||||
});
|
||||
if (moveSelect) {
|
||||
moveSelect.addEventListener('change', function() {
|
||||
if (moveSelect.value) submitBulk('move', moveSelect.value);
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user