setting, session and profile fix
This commit is contained in:
parent
a434af224c
commit
205964841b
4 changed files with 137 additions and 39 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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") },
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue