Files
d 4aa8d7e0b5 feat: chat interactivity overhaul — read receipts, polling, sound, unread, icons
- Read receipts: MarkSenderMessagesReadCrossConv matches by body across conversations
- Anti-fraud: cross-domain read receipts require SHA-256 body proof
- Real-time polling: 5s interval refreshes threads + active thread messages
- Notification sound: Web Audio API beep on new unread messages
- Unread indicator: conv.Unread=true on inbound message delivery
- Double-check icon: custom SVG in web_src/svg/ (survives make svg)
- SMTP icon: octicon-mail for email-delivered messages
- Delivery status: same-domain=M8SH check, external=SMTP mail icon, failed=cross
- Plain mode: global toggle for mobile text selection
- Desktop email layout: no width stripping, mobile-only shrinking
- Registration allowlist: domestic users require admin approval

Assisted-By: Cline:anthropic-claude-sonnet-4-20250514
2026-07-25 09:03:27 +03:00

400 lines
11 KiB
CSS

/* Copyright 2026 The M8SH Authors. */
/* SPDX-License-Identifier: GPL-3.0 */
/* The mail page fills the viewport below the navbar and above the footer.
The body is a flex column: [navbar wrapper .full.height] then [footer].
.full.height has flex-grow:1, so it already fills everything except the footer.
We make it a flex container so the mail page can flex:1 within it, sitting below the navbar. */
/* When the mail page is active, make .full.height a column flex container so
the navbar stays fixed at top and the mail page fills the remaining height. */
.full.height:has(.mail-page) {
display: flex;
flex-direction: column;
min-height: 0;
padding-bottom: 0; /* mail manages its own bottom spacing */
}
.page-content.mail-page {
flex: 1;
min-height: 0;
padding: 0;
display: flex;
flex-direction: column;
}
.page-content.mail-page > #mail-app {
flex: 1;
min-height: 0;
}
/* On mobile, hide the footer so the mail app uses the full viewport height. */
@media (max-width: 639.98px) {
.full.height:has(.mail-page) ~ .page-footer {
display: none;
}
}
/* In-thread search highlight. Global (not scoped) because it must reach
<mark> nodes produced via v-html inside bubbles and html cards. */
#mail-app mark.search-hit {
background: var(--color-highlight-bg);
color: var(--color-text);
border-radius: 2px;
padding: 0 1px;
}
#mail-app mark.search-hit.current {
outline: 1px solid var(--color-highlight-fg);
}
/* Thread context menu. It is teleported to <body> (outside the Vue scoped
component subtree), so its styles live here, unscoped. */
body > .ctx-menu {
position: fixed;
z-index: 1002;
min-width: 140px;
background: var(--color-menu);
border: 0.5px solid var(--color-secondary-alpha-30);
border-radius: var(--border-radius-medium);
padding: 4px;
display: flex;
flex-direction: column;
gap: 2px;
}
body > .ctx-menu .ctx-item {
display: flex;
align-items: center;
gap: 8px;
border: none;
background: transparent;
color: var(--color-text);
font-size: 13px;
padding: 7px 10px;
border-radius: var(--border-radius);
cursor: pointer;
text-align: left;
min-height: 36px;
}
body > .ctx-menu .ctx-item:hover {
background: var(--color-hover);
}
body > .ctx-menu .ctx-item.danger:hover {
color: var(--color-danger);
}
/* Delete conversation confirmation dialog (teleported, so unscoped). */
body > .confirm-overlay {
position: fixed;
inset: 0;
z-index: 1003;
background: var(--color-overlay-backdrop);
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
body > .confirm-overlay .confirm-modal {
width: 100%;
max-width: 360px;
background: var(--color-menu);
border: 0.5px solid var(--color-secondary-alpha-30);
border-radius: var(--border-radius-medium);
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
body > .confirm-overlay .confirm-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: var(--border-radius-full);
background: var(--color-red-badge-bg);
color: var(--color-danger);
}
body > .confirm-overlay .confirm-title {
font-size: 15px;
font-weight: var(--font-weight-medium);
color: var(--color-text);
}
body > .confirm-overlay .confirm-desc {
font-size: 13px;
line-height: 1.4;
color: var(--color-text-light-2);
text-align: center;
}
body > .confirm-overlay .confirm-actions {
display: flex;
gap: 8px;
width: 100%;
margin-top: 4px;
}
body > .confirm-overlay .confirm-cancel {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0.5px solid var(--color-secondary-alpha-30);
background: var(--color-body);
color: var(--color-text);
font-size: 13px;
padding: 8px 12px;
border-radius: var(--border-radius-medium);
cursor: pointer;
min-height: 38px;
}
body > .confirm-overlay .confirm-cancel:hover {
background: var(--color-hover);
}
body > .confirm-overlay .confirm-delete {
flex: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
border: none;
background: var(--color-danger);
color: var(--color-white);
font-size: 13px;
padding: 8px 12px;
border-radius: var(--border-radius-medium);
cursor: pointer;
min-height: 38px;
}
body > .confirm-overlay .confirm-delete:hover {
opacity: 0.9;
}
/* Block toggle row: a right-aligned chevron that flips up when expanded. */
body > .ctx-menu .ctx-toggle .ctx-chev {
margin-left: auto;
color: var(--color-text-light-2);
transition: transform 0.12s ease;
}
body > .ctx-menu .ctx-toggle .ctx-chev.up {
transform: rotate(180deg);
}
/* Expandable Block panel: its option buttons go edge-to-edge on hover so the
whole row is the target, not just the centered label. */
body > .ctx-menu .ctx-expand {
display: flex;
flex-direction: column;
gap: 0;
padding-top: 2px;
border-top: 0.5px solid var(--color-secondary-alpha-30);
margin-top: 2px;
}
body > .ctx-menu .ctx-expand .ctx-opt {
width: 100%;
border-radius: 0;
padding-left: 10px; /* flush with the other menu items' text, no icon column */
}
body > .ctx-menu .ctx-expand .ctx-opt:hover {
width: 100%;
background: var(--color-hover);
}
/* Delete confirmation label inside context menu */
body > .ctx-menu .ctx-confirm-label {
padding: 6px 12px 2px;
font-size: 11px;
color: var(--color-text-light-2);
}
/* Rename group dialog input */
body > .confirm-overlay .rename-input {
width: 100%;
font-size: 14px;
font-family: var(--fonts-regular);
color: var(--color-text);
background: var(--color-secondary-bg);
border: 0.5px solid var(--color-secondary-alpha-30);
border-radius: var(--border-radius-medium);
padding: 8px 10px;
outline: none;
}
body > .confirm-overlay .rename-input:focus {
border-color: var(--color-primary);
}
body > .confirm-overlay .rename-modal {
align-items: stretch;
}
/* Mobile overflow menu: transparent full-screen click catcher with the menu
positioned in the top-right corner, just like the context menu (no darkening). */
body > .overflow-backdrop {
position: fixed;
inset: 0;
z-index: 1002;
}
body > .overflow-backdrop .overflow-menu {
position: absolute;
top: 56px;
right: 8px;
min-width: 160px;
background: var(--color-menu);
border: 0.5px solid var(--color-secondary-alpha-30);
border-radius: var(--border-radius-medium);
padding: 4px;
display: flex;
flex-direction: column;
gap: 2px;
}
body > .overflow-backdrop .ctx-item {
display: flex;
align-items: center;
gap: 8px;
border: none;
background: transparent;
color: var(--color-text);
font-size: 13px;
padding: 7px 10px;
border-radius: var(--border-radius);
cursor: pointer;
text-align: left;
min-height: 36px;
}
body > .overflow-backdrop .ctx-item:hover {
background: var(--color-hover);
}
body > .overflow-backdrop .ctx-item.danger:hover {
color: var(--color-danger);
}
/* Audio player buttons: instant hover, no delay (overrides all inherited transitions) */
#mail-app .audio-player a.ap-btn,
#mail-app .audio-player button.ap-btn,
#mail-app .audio-player a.ap-download,
#mail-app .audio-player .ap-download,
#mail-app .audio-player .ap-close,
#mail-app .audio-player .ap-btn,
#mail-app .audio-player .ap-btn:hover {
transition: none !important;
transition-property: none !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}
/* Mobile: prevent iOS auto-zoom on input focus by ensuring font-size >= 16px
on ALL form elements. The viewport meta override handles most cases, but newer
iOS versions sometimes ignore user-scalable=no, so this is a belt-and-suspenders
fix. Covers textareas, inputs (all types), and contenteditable elements. */
@media (max-width: 639.98px) {
#mail-app input,
#mail-app textarea,
#mail-app select,
#mail-app [contenteditable],
body > .confirm-overlay input,
body > .confirm-overlay textarea,
body > .fwd-overlay input,
body > .fwd-overlay textarea,
body > .ctx-menu input,
body > .overflow-menu input {
font-size: 16px !important;
}
}
/* Interactive UI elements (context menu, overflow menu, thread list items)
always disable text selection. */
#mail-app .thread-item,
#mail-app .ctx-menu,
#mail-app .overflow-menu {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Desktop: message content allows free text selection. */
#mail-app .msg-row,
#mail-app .html-card {
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
}
/* Mobile: disable text selection on ALL message content by default.
This prevents the native system copy/select popup from appearing alongside
our custom context menu on long-press. Only messages explicitly marked with
.plain-msg class (via the "Plain" button) get text selection re-enabled,
which also allows the native copy UI to appear for that specific message.
This is a robust cross-browser solution: user-select:none is universally
supported on iOS Safari, Chrome Android, Firefox, etc. */
@media (max-width: 639.98px) {
#mail-app .msg-row,
#mail-app .html-card {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-touch-callout: none;
}
/* Global plain mode: ALL messages get text selection + native callout.
The .plain-mode class is on the chat-pane root element. */
#mail-app .plain-mode .msg-row,
#mail-app .plain-mode .msg-row .msg-bubble,
#mail-app .plain-mode .msg-row .msg-text,
#mail-app .plain-mode .html-card,
#mail-app .plain-mode .html-card .html-body,
#mail-app .plain-mode .html-body {
-webkit-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
user-select: text !important;
-webkit-touch-callout: default !important;
}
}
/* Prevent double-tap-to-zoom on mobile. touch-action: manipulation allows
panning and pinching but disables double-tap zoom gesture. */
@media (max-width: 639.98px) {
#mail-app,
body > .confirm-overlay,
body > .fwd-overlay,
body > .ctx-menu,
body > .overflow-menu,
body > .att-lightbox {
touch-action: manipulation;
}
}
/* Rendering optimization: content-visibility:auto lets the browser skip
rendering off-screen message bubbles entirely. contain-intrinsic-size
provides a placeholder height so scrollbar calculations work without
actually rendering the content. This makes large conversations smooth
regardless of message count. */
#mail-app .msg-row {
content-visibility: auto;
contain-intrinsic-size: auto 60px;
}
#mail-app .html-card {
content-visibility: auto;
contain-intrinsic-size: auto 120px;
}