Compare commits

...

4 Commits

Author SHA1 Message Date
Ammar Ahmed
4e4d3866df mobile: limit tag to single line only 2026-06-01 12:43:41 +05:00
Ammar Ahmed
b2088ef782 mobile: fix long tag name does not wrap properly in editor 2026-06-01 12:27:37 +05:00
Abdullah Atta
aab6a36067 Do not refetch user before decrypting each chunk in sync (#9805)
* core: do not refetch user before decrypting each chunk in sync

* web: fix editor readonly state not updating in realtime
2026-05-25 19:42:03 +05:00
01zulfi
c7e0cd35e7 editor: expand collapsed nodes on search scroll if search text inside callout (#9745)
* editor: expand callout on search scroll if search text inside callout
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: expand outline list && headings on search scroll
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: expand parents & scroll into view on type

* editor: fix parents expanding on every transaction

* editor: add a small debounce to delay search

* editor: fix editor not scrolling to selected search result

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-05-25 19:41:06 +05:00
5 changed files with 109 additions and 15 deletions

View File

@@ -286,7 +286,8 @@ class EditorStore extends BaseStore<EditorStore> {
const clearIds: string[] = [];
for (const session of sessions) {
if (session.type === "new") continue;
if (session.note.id !== item.id && session.note.contentId !== item.id) continue;
if (session.note.id !== item.id && session.note.contentId !== item.id)
continue;
if (isDeleted(item) || isTrashItem(item))
clearIds.push(session.tabId);
// if a note becomes conflicted, reopen the session
@@ -331,6 +332,13 @@ class EditorStore extends BaseStore<EditorStore> {
!item.readonly
)
openSession(session.note.id, { force: true, silent: true });
// if a note is made readonly, reopen the session
else if (
session.type !== "readonly" &&
item.type === "note" &&
item.readonly
)
openSession(session.note.id, { force: true, silent: true });
// update the note in all sessions
else if (item.type === "note") {
updateSession(

View File

@@ -404,7 +404,9 @@ class UserManager {
const masterKey = await this.getMasterKey();
if (!masterKey) return;
const dataEncryptionKey = await this.keyManager.get("dataEncryptionKey");
const dataEncryptionKey = await this.keyManager.get("dataEncryptionKey", {
refetchUser: false
});
if (!dataEncryptionKey)
return [
{
@@ -415,7 +417,10 @@ class UserManager {
const keys: { version: KeyVersion; key: SerializedKey }[] = [];
const legacyDataEncryptionKey = await this.keyManager.get(
"legacyDataEncryptionKey"
"legacyDataEncryptionKey",
{
refetchUser: false
}
);
if (legacyDataEncryptionKey)
keys.push({

View File

@@ -113,13 +113,19 @@ function Tags(props: { settings: Settings; loading?: boolean }) {
backgroundColor:
index !== 0 ? "transparent" : "var(--nn_secondary_background)",
borderRadius: 6,
padding: "0px 4px",
height: "24px",
padding: "2px 4px",
height: "25px",
fontFamily: "Inter",
fontSize: 12,
color: "var(--nn_primary_icon)",
userSelect: "none",
WebkitUserSelect: "none"
WebkitUserSelect: "none",
textAlign: "left",
maxWidth: 150,
textOverflow: "ellipsis",
overflow: "hidden",
wordWrap: "break-word",
whiteSpace: "nowrap"
}}
onClick={(e) => {
e.preventDefault();

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Extension } from "@tiptap/core";
import { Decoration, DecorationSet } from "prosemirror-view";
import { Decoration, DecorationSet, EditorView } from "prosemirror-view";
import {
EditorState,
Plugin,
@@ -28,6 +28,7 @@ import {
} from "prosemirror-state";
import { SearchSettings } from "../../toolbar/stores/search-store.js";
import { tiptapKeys } from "@notesnook/common";
import { toggleNodesUnderPos } from "../heading/index.js";
type DispatchFn = (tr: Transaction) => void;
declare module "@tiptap/core" {
@@ -295,12 +296,11 @@ export const SearchReplace = Extension.create<SearchOptions, SearchStorage>({
)
);
const domNode = this.editor.view.domAtPos(from).node;
scrollIntoView(domNode);
this.storage.selectedIndex = nextIndex;
tr.setMeta("isSearching", true);
tr.setMeta("selectedIndex", nextIndex);
if (dispatch) updateView(state, dispatch);
return true;
},
moveToPreviousResult:
@@ -322,10 +322,8 @@ export const SearchReplace = Extension.create<SearchOptions, SearchStorage>({
)
);
const domNode = this.editor.view.domAtPos(from).node;
scrollIntoView(domNode);
this.storage.selectedIndex = prevIndex;
tr.setMeta("isSearching", true);
tr.setMeta("selectedIndex", prevIndex);
if (dispatch) updateView(state, dispatch);
@@ -470,14 +468,90 @@ export const SearchReplace = Extension.create<SearchOptions, SearchStorage>({
decorations(state) {
return key.getState(state).results;
}
},
appendTransaction: (transactions, oldState, newState) => {
const isSearchTransaction = transactions.find((t) =>
t.getMeta("isSearching")
);
const selectedResult =
this.storage.results?.[this.storage.selectedIndex];
if (!isSearchTransaction || !selectedResult) return;
const tr = newState.tr;
scrollIntoView(this.editor.view, selectedResult.from);
if (expandCollapsedParents(tr, selectedResult.from)) {
return tr;
}
}
})
];
}
});
function scrollIntoView(domNode: Node) {
function expandCollapsedParents(tr: Transaction, pos: number) {
try {
let changed = false;
const $pos = tr.doc.resolve(pos);
for (let depth = 1; depth <= $pos.depth; depth++) {
const node = $pos.node(depth);
const nodePos = $pos.before(depth);
if (
(node.type.name === "callout" ||
node.type.name === "outlineListItem") &&
node.attrs.collapsed
) {
tr.setNodeAttribute(nodePos, "collapsed", false);
changed = true;
}
// expand collapsed heading that hid this node via hidden attribute
if (node.attrs.hidden) {
const parentNode = $pos.node(depth - 1);
const parentContentStart = depth === 1 ? 0 : $pos.before(depth - 1) + 1;
let collapsedHeadingPos = -1;
let collapsedHeadingLevel = -1;
parentNode.forEach((child, offset) => {
const childAbsPos = parentContentStart + offset;
if (childAbsPos >= nodePos) return;
if (
child.type.name === "heading" &&
child.attrs.collapsed &&
!child.attrs.hidden
) {
collapsedHeadingPos = childAbsPos;
collapsedHeadingLevel = child.attrs.level;
}
});
if (collapsedHeadingPos !== -1) {
tr.setNodeAttribute(collapsedHeadingPos, "collapsed", false);
toggleNodesUnderPos(
tr,
collapsedHeadingPos,
collapsedHeadingLevel,
false
);
changed = true;
}
}
}
if (changed) tr.setMeta("preventSave", true);
return changed;
} catch (e) {
console.error("Error expanding collapsed parents: ", e);
}
}
function scrollIntoView(view: EditorView, pos: number) {
setTimeout(() => {
const domNode = view.domAtPos(pos).node;
if ("scrollIntoView" in domNode) {
(domNode as Element).scrollIntoView({
behavior: "instant",

View File

@@ -25,6 +25,7 @@ import { ToolButton } from "../components/tool-button.js";
import { Editor } from "../../types.js";
import { useEditorSearchStore } from "../stores/search-store.js";
import { strings } from "@notesnook/intl";
import { inlineDebounce } from "@notesnook/common";
export type SearchReplacePopupProps = { editor: Editor };
export function SearchReplacePopup(props: SearchReplacePopupProps) {
@@ -94,7 +95,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
sx={{ p: 0, fontFamily: "monospace" }}
value={searchTerm}
onChange={(e) => {
search(e.target.value);
inlineDebounce("search", () => search(e.target.value), 100);
useEditorSearchStore.setState({ searchTerm: e.target.value });
}}
onKeyDown={(e) => {