Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/client/src/layouts/desktop_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import ProtectedSessionLockScreen from "../widgets/ProtectedSessionLockScreen.jsx";
import QuickSearchWidget from "../widgets/quick_search.js";
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
import { FixedFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.jsx";
Expand Down Expand Up @@ -187,6 +188,7 @@ export default class DesktopLayout {
)
.optChild(launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
.child(<CloseZenModeButton />)
.child(<ProtectedSessionLockScreen />)

// Desktop-specific dialogs.
.child(<PasswordNoteSetDialog />)
Expand Down
4 changes: 3 additions & 1 deletion apps/client/src/layouts/mobile_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import NoteTitleWidget from "../widgets/note_title.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import ProtectedSessionLockScreen from "../widgets/ProtectedSessionLockScreen.jsx";
import QuickSearchWidget from "../widgets/quick_search.js";
import ScrollPadding from "../widgets/scroll_padding";
import SearchResult from "../widgets/search_result.jsx";
Expand Down Expand Up @@ -94,7 +95,8 @@ export default class MobileLayout {
.child(<GlobalMenuWidget isHorizontalLayout />)
.id("launcher-pane"))
)
.child(<CloseZenModeButton />);
.child(<CloseZenModeButton />)
.child(<ProtectedSessionLockScreen />);
applyModals(rootContainer);
return rootContainer;
}
Expand Down
36 changes: 36 additions & 0 deletions apps/client/src/widgets/ProtectedSessionLockScreen.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.protected-session-lock-screen {
position: fixed;
inset: 0;
z-index: 10001;
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
padding-top: max(24px, env(safe-area-inset-top));
padding-right: max(20px, env(safe-area-inset-right));
padding-bottom: max(24px, env(safe-area-inset-bottom));
padding-left: max(20px, env(safe-area-inset-left));
background: rgb(0 0 0 / 0.35);
backdrop-filter: blur(10px);
-webkit-app-region: no-drag;
}

.protected-session-lock-screen__panel {
width: min(100%, 560px);
padding: 32px 24px;
border: 1px solid var(--main-border-color);
border-radius: 20px;
background: var(--modal-background-color, var(--main-background-color));
box-shadow: 0 24px 60px rgb(0 0 0 / 0.25);
}

.protected-session-lock-screen .protected-session-password-form {
margin-bottom: 0;
}

@media (max-width: 575px) {
.protected-session-lock-screen__panel {
padding: 28px 20px;
border-radius: 16px;
}
}
78 changes: 78 additions & 0 deletions apps/client/src/widgets/ProtectedSessionLockScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import "./ProtectedSessionLockScreen.css";

import { useCallback, useEffect, useRef, useState } from "preact/hooks";

import froca from "../services/froca.js";
import { t } from "../services/i18n.js";
import protected_session_holder from "../services/protected_session_holder.js";
import { useTriliumEvent } from "./react/hooks.jsx";
import ProtectedSession from "./type_widgets/ProtectedSession.jsx";

export default function ProtectedSessionLockScreen() {
const [ isRootProtected, setIsRootProtected ] = useState(froca.getNoteFromCache("root")?.isProtected ?? false);
const [ isProtectedSessionAvailable, setIsProtectedSessionAvailable ] = useState(protected_session_holder.isProtectedSessionAvailable());
const requestIdRef = useRef(0);

const refreshRootProtection = useCallback(() => {
const cachedRootNote = froca.getNoteFromCache("root");

if (cachedRootNote) {
requestIdRef.current++;
setIsRootProtected(cachedRootNote.isProtected ?? false);
return;
}

const requestId = ++requestIdRef.current;
froca.getNote("root")
.then((rootNote) => {
if (requestId !== requestIdRef.current) {
return;
}

setIsRootProtected(rootNote?.isProtected ?? false);
})
.catch((error: unknown) => {
if (requestId !== requestIdRef.current) {
return;
}

const message = error instanceof Error ? error.message : String(error);
window.logError(`Failed to load root note protection state: ${message}`);
setIsRootProtected(false);
});
}, []);

useEffect(() => {
refreshRootProtection();
}, [ refreshRootProtection ]);

useTriliumEvent("frocaReloaded", () => {
refreshRootProtection();
setIsProtectedSessionAvailable(protected_session_holder.isProtectedSessionAvailable());
});
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.isNoteReloaded("root")) {
refreshRootProtection();
}
});
useTriliumEvent("protectedSessionStarted", () => {
setIsProtectedSessionAvailable(protected_session_holder.isProtectedSessionAvailable());
});

if (!isRootProtected || isProtectedSessionAvailable) {
return null;
}

return (
<div
class="protected-session-lock-screen"
role="dialog"
aria-modal="true"
aria-label={t("protected_session_password.modal_title")}
>
<div class="protected-session-lock-screen__panel">
<ProtectedSession autoFocus />
</div>
</div>
);
}
22 changes: 15 additions & 7 deletions apps/client/src/widgets/type_widgets/ProtectedSession.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { useCallback, useRef } from "preact/hooks";
import "./ProtectedSession.css";

import type { TargetedSubmitEvent } from "preact";
import { useCallback, useEffect, useRef } from "preact/hooks";

import { t } from "../../services/i18n";
import protected_session from "../../services/protected_session";
import Button from "../react/Button";
import FormGroup from "../react/FormGroup";
import FormTextBox from "../react/FormTextBox";
import "./ProtectedSession.css";
import protected_session from "../../services/protected_session";
import type { TargetedSubmitEvent } from "preact";

export default function ProtectedSession() {
export default function ProtectedSession({ autoFocus = false }: { autoFocus?: boolean }) {
const passwordRef = useRef<HTMLInputElement>(null);

useEffect(() => {
if (autoFocus) {
passwordRef.current?.focus();
}
}, [ autoFocus ]);

const submitCallback = useCallback((e: TargetedSubmitEvent<HTMLFormElement>) => {
if (!passwordRef.current) return;
e.preventDefault();
Expand Down Expand Up @@ -38,5 +46,5 @@ export default function ProtectedSession() {
keyboardShortcut="Enter"
/>
</form>
)
}
);
}
Loading