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 SignupPage from "./components/SignupPage";
|
||||||
import SmtpCredentialsPage from "./components/SmtpCredentialsPage";
|
import SmtpCredentialsPage from "./components/SmtpCredentialsPage";
|
||||||
import WebLanding from "./components/WebLanding";
|
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() {
|
function useLocationPath() {
|
||||||
const [path, setPath] = useState(currentPath);
|
const [path, setPath] = useState(currentPath);
|
||||||
|
|
@ -33,7 +33,7 @@ export default function App() {
|
||||||
}, [authed, path]);
|
}, [authed, path]);
|
||||||
|
|
||||||
useEffect(() => onSessionChange(() => {
|
useEffect(() => onSessionChange(() => {
|
||||||
window.location.reload();
|
followSharedSession();
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
if (showApp) {
|
if (showApp) {
|
||||||
|
|
|
||||||
|
|
@ -2,48 +2,48 @@ export const AUTH_TOKEN_KEY = "agmail_access_token";
|
||||||
export const AUTH_ACCOUNT_KEY = "agmail_account";
|
export const AUTH_ACCOUNT_KEY = "agmail_account";
|
||||||
|
|
||||||
export const APP_HOME_PATH = "/app";
|
export const APP_HOME_PATH = "/app";
|
||||||
|
export const LOGIN_PATH = "/login";
|
||||||
export const SESSION_GATED_PATHS = new Set(["/", "/login", "/signup", "/forgot-password"]);
|
export const SESSION_GATED_PATHS = new Set(["/", "/login", "/signup", "/forgot-password"]);
|
||||||
|
|
||||||
|
const AUTH_CHANNEL = "agmail-auth";
|
||||||
let sessionRedirecting = false;
|
let sessionRedirecting = false;
|
||||||
|
let authChannel = null;
|
||||||
|
|
||||||
|
function sharedStore() {
|
||||||
|
return localStorage;
|
||||||
|
}
|
||||||
|
|
||||||
function readShared(key) {
|
function readShared(key) {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(key);
|
return sharedStore().getItem(key);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeShared(key, value) {
|
function dropTabCopies() {
|
||||||
localStorage.setItem(key, value);
|
sessionStorage.removeItem(AUTH_TOKEN_KEY);
|
||||||
sessionStorage.setItem(key, value);
|
sessionStorage.removeItem(AUTH_ACCOUNT_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeShared(key) {
|
dropTabCopies();
|
||||||
localStorage.removeItem(key);
|
|
||||||
sessionStorage.removeItem(key);
|
function channel() {
|
||||||
|
if (authChannel || typeof BroadcastChannel === "undefined") return authChannel;
|
||||||
|
authChannel = new BroadcastChannel(AUTH_CHANNEL);
|
||||||
|
return authChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncSharedSession() {
|
function publish(type) {
|
||||||
const sharedToken = readShared(AUTH_TOKEN_KEY);
|
try {
|
||||||
if (sharedToken) {
|
channel()?.postMessage({ type });
|
||||||
sessionStorage.setItem(AUTH_TOKEN_KEY, sharedToken);
|
} catch {
|
||||||
const sharedAccount = readShared(AUTH_ACCOUNT_KEY);
|
/* private browsing can reject BroadcastChannel */
|
||||||
if (sharedAccount) sessionStorage.setItem(AUTH_ACCOUNT_KEY, sharedAccount);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
export function getAccessToken() {
|
||||||
return readShared(AUTH_TOKEN_KEY) || sessionStorage.getItem(AUTH_TOKEN_KEY);
|
return readShared(AUTH_TOKEN_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isLoggedIn() {
|
export function isLoggedIn() {
|
||||||
|
|
@ -51,27 +51,55 @@ export function isLoggedIn() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveSession(token, account) {
|
export function saveSession(token, account) {
|
||||||
writeShared(AUTH_TOKEN_KEY, token);
|
sharedStore().setItem(AUTH_TOKEN_KEY, token);
|
||||||
if (account) writeShared(AUTH_ACCOUNT_KEY, JSON.stringify(account));
|
if (account) sharedStore().setItem(AUTH_ACCOUNT_KEY, JSON.stringify(account));
|
||||||
|
dropTabCopies();
|
||||||
|
publish("login");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveStoredAccount(account) {
|
export function saveStoredAccount(account) {
|
||||||
writeShared(AUTH_ACCOUNT_KEY, JSON.stringify(account));
|
sharedStore().setItem(AUTH_ACCOUNT_KEY, JSON.stringify(account));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearSession() {
|
export function clearSession() {
|
||||||
removeShared(AUTH_TOKEN_KEY);
|
sharedStore().removeItem(AUTH_TOKEN_KEY);
|
||||||
removeShared(AUTH_ACCOUNT_KEY);
|
sharedStore().removeItem(AUTH_ACCOUNT_KEY);
|
||||||
|
dropTabCopies();
|
||||||
|
publish("logout");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onSessionChange(listener) {
|
export function onSessionChange(listener) {
|
||||||
function handle(event) {
|
function handleStorage(event) {
|
||||||
if (event.storageArea !== localStorage) return;
|
if (event.storageArea !== localStorage) return;
|
||||||
if (event.key !== AUTH_TOKEN_KEY && event.key !== AUTH_ACCOUNT_KEY && event.key !== null) return;
|
if (event.key !== AUTH_TOKEN_KEY && event.key !== null) return;
|
||||||
listener(event);
|
listener(event.newValue || null);
|
||||||
}
|
}
|
||||||
window.addEventListener("storage", handle);
|
function handleBroadcast(event) {
|
||||||
return () => window.removeEventListener("storage", handle);
|
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() {
|
export function currentPath() {
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,42 @@ function detectWarnings({ bodyHtml, to, cc, bcc, attachments, orgDomain }) {
|
||||||
return warnings;
|
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({
|
function MenuDropdown({
|
||||||
label,
|
label,
|
||||||
items,
|
items,
|
||||||
|
|
@ -87,6 +123,7 @@ function MenuDropdown({
|
||||||
className = "",
|
className = "",
|
||||||
showCaret = true,
|
showCaret = true,
|
||||||
iconStroke = 1.7,
|
iconStroke = 1.7,
|
||||||
|
onOpen,
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
|
@ -101,7 +138,18 @@ function MenuDropdown({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`compose-menu ${className}`} ref={ref}>
|
<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}
|
{label}
|
||||||
{showCaret && <ChevronDown size={14} strokeWidth={iconStroke} />}
|
{showCaret && <ChevronDown size={14} strokeWidth={iconStroke} />}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -301,6 +349,7 @@ export default function ComposeModal({
|
||||||
const draftHydratedRef = useRef(false);
|
const draftHydratedRef = useRef(false);
|
||||||
const savedRangeRef = useRef(null);
|
const savedRangeRef = useRef(null);
|
||||||
const linkPopoverRef = useRef(null);
|
const linkPopoverRef = useRef(null);
|
||||||
|
const emojiWrapRef = useRef(null);
|
||||||
|
|
||||||
const [showCc, setShowCc] = useState(false);
|
const [showCc, setShowCc] = useState(false);
|
||||||
const [showBcc, setShowBcc] = useState(false);
|
const [showBcc, setShowBcc] = useState(false);
|
||||||
|
|
@ -415,6 +464,17 @@ export default function ComposeModal({
|
||||||
});
|
});
|
||||||
}, [account.email]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (draftTimerRef.current) window.clearTimeout(draftTimerRef.current);
|
if (draftTimerRef.current) window.clearTimeout(draftTimerRef.current);
|
||||||
draftTimerRef.current = window.setTimeout(saveDraft, 1400);
|
draftTimerRef.current = window.setTimeout(saveDraft, 1400);
|
||||||
|
|
@ -511,6 +571,10 @@ export default function ComposeModal({
|
||||||
if (next && next !== editor) next.style.textAlign = align;
|
if (next && next !== editor) next.style.textAlign = align;
|
||||||
normalizeEditorAlignment(editor);
|
normalizeEditorAlignment(editor);
|
||||||
}
|
}
|
||||||
|
} else if (command === "removeFormat") {
|
||||||
|
unlinkSelection(editorRef.current);
|
||||||
|
document.execCommand("unlink", false, null);
|
||||||
|
document.execCommand("removeFormat", false, value);
|
||||||
} else {
|
} else {
|
||||||
document.execCommand(command, false, value);
|
document.execCommand(command, false, value);
|
||||||
}
|
}
|
||||||
|
|
@ -765,6 +829,7 @@ export default function ComposeModal({
|
||||||
className="compose-from-picker"
|
className="compose-from-picker"
|
||||||
showCaret
|
showCaret
|
||||||
iconStroke={iconStroke}
|
iconStroke={iconStroke}
|
||||||
|
onOpen={() => setShowEmoji(false)}
|
||||||
label={
|
label={
|
||||||
<span className="compose-from-label">
|
<span className="compose-from-label">
|
||||||
<strong>{fromAddress.email}</strong>
|
<strong>{fromAddress.email}</strong>
|
||||||
|
|
@ -1027,13 +1092,14 @@ export default function ComposeModal({
|
||||||
>
|
>
|
||||||
<Link size={16} strokeWidth={iconStroke} />
|
<Link size={16} strokeWidth={iconStroke} />
|
||||||
</button>
|
</button>
|
||||||
<div className="compose-emoji-wrap">
|
<div className="compose-emoji-wrap" ref={emojiWrapRef}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="compose-icon-btn"
|
className={`compose-icon-btn${showEmoji ? " active" : ""}`}
|
||||||
title="Emoji"
|
title="Emoji"
|
||||||
|
aria-expanded={showEmoji}
|
||||||
onMouseDown={(event) => event.preventDefault()}
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
onClick={() => setShowEmoji((v) => !v)}
|
onClick={() => setShowEmoji((open) => !open)}
|
||||||
>
|
>
|
||||||
<Smile size={16} strokeWidth={iconStroke} />
|
<Smile size={16} strokeWidth={iconStroke} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1049,6 +1115,7 @@ export default function ComposeModal({
|
||||||
className="compose-icon-menu"
|
className="compose-icon-menu"
|
||||||
showCaret={false}
|
showCaret={false}
|
||||||
iconStroke={iconStroke}
|
iconStroke={iconStroke}
|
||||||
|
onOpen={() => setShowEmoji(false)}
|
||||||
label={<PenLine size={16} strokeWidth={iconStroke} />}
|
label={<PenLine size={16} strokeWidth={iconStroke} />}
|
||||||
items={signatures.map((signature) => ({
|
items={signatures.map((signature) => ({
|
||||||
id: signature.id,
|
id: signature.id,
|
||||||
|
|
@ -1060,6 +1127,7 @@ export default function ComposeModal({
|
||||||
className="compose-icon-menu"
|
className="compose-icon-menu"
|
||||||
showCaret={false}
|
showCaret={false}
|
||||||
iconStroke={iconStroke}
|
iconStroke={iconStroke}
|
||||||
|
onOpen={() => setShowEmoji(false)}
|
||||||
label={<Lock size={16} strokeWidth={iconStroke} />}
|
label={<Lock size={16} strokeWidth={iconStroke} />}
|
||||||
items={[
|
items={[
|
||||||
{ id: "standard", label: "Standard", active: true, onClick: () => flashNotice("Standard security") },
|
{ id: "standard", label: "Standard", active: true, onClick: () => flashNotice("Standard security") },
|
||||||
|
|
@ -1071,6 +1139,7 @@ export default function ComposeModal({
|
||||||
className="compose-icon-menu"
|
className="compose-icon-menu"
|
||||||
showCaret={false}
|
showCaret={false}
|
||||||
iconStroke={iconStroke}
|
iconStroke={iconStroke}
|
||||||
|
onOpen={() => setShowEmoji(false)}
|
||||||
label={<MoreHorizontal size={16} strokeWidth={iconStroke} />}
|
label={<MoreHorizontal size={16} strokeWidth={iconStroke} />}
|
||||||
items={[
|
items={[
|
||||||
{ id: "receipt", label: "Request read receipt", onClick: () => flashNotice("Read receipt — coming soon") },
|
{ 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-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,.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-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 { 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.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; }
|
.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