setting, session and profile fix

This commit is contained in:
debu-2 2026-10-07 06:39:07 +05:30
parent a434af224c
commit 205964841b
4 changed files with 137 additions and 39 deletions

View file

@ -7,7 +7,7 @@ import MobileLanding from "./components/MobileLanding";
import SignupPage from "./components/SignupPage";
import SmtpCredentialsPage from "./components/SmtpCredentialsPage";
import WebLanding from "./components/WebLanding";
import { APP_HOME_PATH, currentPath, enterApp, isLoggedIn, onSessionChange, SESSION_GATED_PATHS } from "./auth";
import { APP_HOME_PATH, currentPath, enterApp, followSharedSession, isLoggedIn, onSessionChange, SESSION_GATED_PATHS } from "./auth";
function useLocationPath() {
const [path, setPath] = useState(currentPath);
@ -33,7 +33,7 @@ export default function App() {
}, [authed, path]);
useEffect(() => onSessionChange(() => {
window.location.reload();
followSharedSession();
}), []);
if (showApp) {

View file

@ -2,48 +2,48 @@ export const AUTH_TOKEN_KEY = "agmail_access_token";
export const AUTH_ACCOUNT_KEY = "agmail_account";
export const APP_HOME_PATH = "/app";
export const LOGIN_PATH = "/login";
export const SESSION_GATED_PATHS = new Set(["/", "/login", "/signup", "/forgot-password"]);
const AUTH_CHANNEL = "agmail-auth";
let sessionRedirecting = false;
let authChannel = null;
function sharedStore() {
return localStorage;
}
function readShared(key) {
try {
return localStorage.getItem(key);
return sharedStore().getItem(key);
} catch {
return null;
}
}
function writeShared(key, value) {
localStorage.setItem(key, value);
sessionStorage.setItem(key, value);
function dropTabCopies() {
sessionStorage.removeItem(AUTH_TOKEN_KEY);
sessionStorage.removeItem(AUTH_ACCOUNT_KEY);
}
function removeShared(key) {
localStorage.removeItem(key);
sessionStorage.removeItem(key);
dropTabCopies();
function channel() {
if (authChannel || typeof BroadcastChannel === "undefined") return authChannel;
authChannel = new BroadcastChannel(AUTH_CHANNEL);
return authChannel;
}
function syncSharedSession() {
const sharedToken = readShared(AUTH_TOKEN_KEY);
if (sharedToken) {
sessionStorage.setItem(AUTH_TOKEN_KEY, sharedToken);
const sharedAccount = readShared(AUTH_ACCOUNT_KEY);
if (sharedAccount) sessionStorage.setItem(AUTH_ACCOUNT_KEY, sharedAccount);
return;
function publish(type) {
try {
channel()?.postMessage({ type });
} catch {
/* private browsing can reject BroadcastChannel */
}
const legacyToken = sessionStorage.getItem(AUTH_TOKEN_KEY);
if (!legacyToken) return;
localStorage.setItem(AUTH_TOKEN_KEY, legacyToken);
const legacyAccount = sessionStorage.getItem(AUTH_ACCOUNT_KEY);
if (legacyAccount) localStorage.setItem(AUTH_ACCOUNT_KEY, legacyAccount);
}
syncSharedSession();
export function getAccessToken() {
return readShared(AUTH_TOKEN_KEY) || sessionStorage.getItem(AUTH_TOKEN_KEY);
return readShared(AUTH_TOKEN_KEY);
}
export function isLoggedIn() {
@ -51,27 +51,55 @@ export function isLoggedIn() {
}
export function saveSession(token, account) {
writeShared(AUTH_TOKEN_KEY, token);
if (account) writeShared(AUTH_ACCOUNT_KEY, JSON.stringify(account));
sharedStore().setItem(AUTH_TOKEN_KEY, token);
if (account) sharedStore().setItem(AUTH_ACCOUNT_KEY, JSON.stringify(account));
dropTabCopies();
publish("login");
}
export function saveStoredAccount(account) {
writeShared(AUTH_ACCOUNT_KEY, JSON.stringify(account));
sharedStore().setItem(AUTH_ACCOUNT_KEY, JSON.stringify(account));
}
export function clearSession() {
removeShared(AUTH_TOKEN_KEY);
removeShared(AUTH_ACCOUNT_KEY);
sharedStore().removeItem(AUTH_TOKEN_KEY);
sharedStore().removeItem(AUTH_ACCOUNT_KEY);
dropTabCopies();
publish("logout");
}
export function onSessionChange(listener) {
function handle(event) {
function handleStorage(event) {
if (event.storageArea !== localStorage) return;
if (event.key !== AUTH_TOKEN_KEY && event.key !== AUTH_ACCOUNT_KEY && event.key !== null) return;
listener(event);
if (event.key !== AUTH_TOKEN_KEY && event.key !== null) return;
listener(event.newValue || null);
}
window.addEventListener("storage", handle);
return () => window.removeEventListener("storage", handle);
function handleBroadcast(event) {
if (event.data?.type === "logout") listener(null);
if (event.data?.type === "login") listener(getAccessToken());
}
window.addEventListener("storage", handleStorage);
channel()?.addEventListener("message", handleBroadcast);
return () => {
window.removeEventListener("storage", handleStorage);
channel()?.removeEventListener("message", handleBroadcast);
};
}
export function followSharedSession() {
if (sessionRedirecting) return;
if (isLoggedIn()) {
if (currentPath() !== APP_HOME_PATH) {
sessionRedirecting = true;
window.location.replace(APP_HOME_PATH);
} else {
window.location.reload();
}
return;
}
if (currentPath() === LOGIN_PATH) return;
sessionRedirecting = true;
window.location.replace(LOGIN_PATH);
}
export function currentPath() {

View file

@ -80,6 +80,42 @@ function detectWarnings({ bodyHtml, to, cc, bcc, attachments, orgDomain }) {
return warnings;
}
function unwrapElement(element) {
const parent = element.parentNode;
if (!parent) return;
while (element.firstChild) parent.insertBefore(element.firstChild, element);
parent.removeChild(element);
}
function unlinkSelection(editor) {
if (!editor) return;
const current = selectionInside(editor);
if (!current) {
editor.querySelectorAll("a").forEach(unwrapElement);
return;
}
const { selection, range } = current;
if (range.collapsed) {
const node = range.startContainer;
const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
const anchor = element?.closest?.("a");
if (anchor && editor.contains(anchor)) unwrapElement(anchor);
return;
}
const anchors = [...editor.querySelectorAll("a")].filter((anchor) => {
try {
return range.intersectsNode(anchor);
} catch {
return false;
}
});
if (anchors.length) {
anchors.forEach(unwrapElement);
return;
}
document.execCommand("unlink", false, null);
}
function MenuDropdown({
label,
items,
@ -87,6 +123,7 @@ function MenuDropdown({
className = "",
showCaret = true,
iconStroke = 1.7,
onOpen,
}) {
const [open, setOpen] = useState(false);
const ref = useRef(null);
@ -101,7 +138,18 @@ function MenuDropdown({
return (
<div className={`compose-menu ${className}`} ref={ref}>
<button type="button" className="compose-menu-trigger" onClick={() => setOpen((v) => !v)} aria-expanded={open}>
<button
type="button"
className="compose-menu-trigger"
aria-expanded={open}
onClick={() => {
setOpen((current) => {
const next = !current;
if (next) onOpen?.();
return next;
});
}}
>
{label}
{showCaret && <ChevronDown size={14} strokeWidth={iconStroke} />}
</button>
@ -301,6 +349,7 @@ export default function ComposeModal({
const draftHydratedRef = useRef(false);
const savedRangeRef = useRef(null);
const linkPopoverRef = useRef(null);
const emojiWrapRef = useRef(null);
const [showCc, setShowCc] = useState(false);
const [showBcc, setShowBcc] = useState(false);
@ -415,6 +464,17 @@ export default function ComposeModal({
});
}, [account.email]);
useEffect(() => {
if (!showEmoji) return undefined;
function closeEmoji(event) {
if (!emojiWrapRef.current?.contains(event.target)) {
setShowEmoji(false);
}
}
document.addEventListener("mousedown", closeEmoji);
return () => document.removeEventListener("mousedown", closeEmoji);
}, [showEmoji]);
useEffect(() => {
if (draftTimerRef.current) window.clearTimeout(draftTimerRef.current);
draftTimerRef.current = window.setTimeout(saveDraft, 1400);
@ -511,6 +571,10 @@ export default function ComposeModal({
if (next && next !== editor) next.style.textAlign = align;
normalizeEditorAlignment(editor);
}
} else if (command === "removeFormat") {
unlinkSelection(editorRef.current);
document.execCommand("unlink", false, null);
document.execCommand("removeFormat", false, value);
} else {
document.execCommand(command, false, value);
}
@ -765,6 +829,7 @@ export default function ComposeModal({
className="compose-from-picker"
showCaret
iconStroke={iconStroke}
onOpen={() => setShowEmoji(false)}
label={
<span className="compose-from-label">
<strong>{fromAddress.email}</strong>
@ -1027,13 +1092,14 @@ export default function ComposeModal({
>
<Link size={16} strokeWidth={iconStroke} />
</button>
<div className="compose-emoji-wrap">
<div className="compose-emoji-wrap" ref={emojiWrapRef}>
<button
type="button"
className="compose-icon-btn"
className={`compose-icon-btn${showEmoji ? " active" : ""}`}
title="Emoji"
aria-expanded={showEmoji}
onMouseDown={(event) => event.preventDefault()}
onClick={() => setShowEmoji((v) => !v)}
onClick={() => setShowEmoji((open) => !open)}
>
<Smile size={16} strokeWidth={iconStroke} />
</button>
@ -1049,6 +1115,7 @@ export default function ComposeModal({
className="compose-icon-menu"
showCaret={false}
iconStroke={iconStroke}
onOpen={() => setShowEmoji(false)}
label={<PenLine size={16} strokeWidth={iconStroke} />}
items={signatures.map((signature) => ({
id: signature.id,
@ -1060,6 +1127,7 @@ export default function ComposeModal({
className="compose-icon-menu"
showCaret={false}
iconStroke={iconStroke}
onOpen={() => setShowEmoji(false)}
label={<Lock size={16} strokeWidth={iconStroke} />}
items={[
{ id: "standard", label: "Standard", active: true, onClick: () => flashNotice("Standard security") },
@ -1071,6 +1139,7 @@ export default function ComposeModal({
className="compose-icon-menu"
showCaret={false}
iconStroke={iconStroke}
onOpen={() => setShowEmoji(false)}
label={<MoreHorizontal size={16} strokeWidth={iconStroke} />}
items={[
{ id: "receipt", label: "Request read receipt", onClick: () => flashNotice("Read receipt — coming soon") },

View file

@ -1198,6 +1198,7 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.compose-menu-trigger { min-height:34px; padding:0 12px; display:inline-flex; align-items:center; gap:6px; border:0; border-radius:8px; background:var(--coral); color:var(--ink); font-size:12px; font-weight:700; }
.compose-icon-menu .compose-menu-trigger,.compose-icon-btn { width:32px; height:32px; padding:0; display:grid; place-items:center; border:0; border-radius:6px; background:transparent; color:#6f6a73; box-shadow:none; }
.compose-icon-menu .compose-menu-trigger:hover,.compose-icon-btn:hover { background:#f3efec; color:var(--ink); }
.compose-icon-btn.active { background:#ece7e1; color:#241f2b; }
.compose-menu-panel { position:absolute; bottom:calc(100% + 6px); left:0; z-index:20; min-width:190px; padding:6px; display:grid; gap:2px; border:1px solid #ECE9EE; border-radius:8px; background:#fff; box-shadow:0 8px 24px rgba(20,18,30,.12); }
.compose-menu-panel.align-right { left:auto; right:0; }
.compose-menu-panel button { width:100%; min-height:34px; padding:0 10px; display:flex; align-items:center; gap:8px; border:0; border-radius:6px; background:transparent; color:var(--ink); font-size:11px; font-weight:600; text-align:left; }