Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make search more usuable #62

Merged
merged 12 commits into from
Apr 22, 2023
Merged
15 changes: 8 additions & 7 deletions packages/marqus-desktop/src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
Section,
SerializedAppState,
} from "../shared/ui/app";
import { getNoteById, Note } from "../shared/domain/note";
import { flatten, getNoteById, Note } from "../shared/domain/note";
import { State, Listener, useStore } from "./store";
import { isTest } from "../shared/env";
import { isEmpty, tail } from "lodash";
Expand All @@ -25,8 +25,9 @@ import { Config } from "../shared/domain/config";
import { log } from "./logger";
import { arrayify } from "../shared/utils";
import { NoteDirectoryModal } from "./components/NoteDirectoryModal";
import { searchNotes } from "./components/SidebarSearch";
import { Shortcut } from "../shared/domain/shortcut";
import { MatchData, search } from "fast-fuzzy";
import { FUZZY_OPTIONS } from "./components/SidebarSearch";

async function main() {
let config: Config;
Expand Down Expand Up @@ -166,11 +167,11 @@ export async function loadInitialState(
}))
.filter(t => t.note != null) as EditorTab[];

// TODO: Find a better option than this. I suspect we need to refactor our store
// a bit to support external state better.
const searchResults = searchNotes(notes, ui.sidebar.searchString ?? "").map(
n => n.id,
);
let searchResults: MatchData<Note>[] = [];
if (ui.sidebar.searchString) {
const flatNotes = flatten(notes);
searchResults = search(ui.sidebar.searchString, flatNotes, FUZZY_OPTIONS);
}

const deserializedAppState = filterOutStaleNoteIds(
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,11 @@ export const revealTabNoteInSidebar: Listener<
});
};

export function openTabsForNotes(ctx: StoreContext, noteIds: string[]): void {
export function openTabsForNotes(
ctx: StoreContext,
noteIds: string[],
newActiveTabNoteId?: string,
): void {
if (noteIds.length === 0) {
return;
}
Expand Down Expand Up @@ -740,6 +744,9 @@ export function openTabsForNotes(ctx: StoreContext, noteIds: string[]): void {
}

let { activeTabNoteId } = editor;
if (newActiveTabNoteId) {
activeTabNoteId = newActiveTabNoteId;
}
if (!activeTabNoteId) {
activeTabNoteId = tabs[0].note.id;
}
Expand Down
2 changes: 0 additions & 2 deletions packages/marqus-desktop/src/renderer/components/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import { Listener, Store } from "../store";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const emoji = require("remark-emoji");

const LINE_HEIGHT = 30;

type ImageAlign = "left" | "right" | "center";

export interface MarkdownProps {
Expand Down
20 changes: 7 additions & 13 deletions packages/marqus-desktop/src/renderer/components/Monaco.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Section } from "../../shared/ui/app";
import { Attachment, Protocol } from "../../shared/domain/protocols";
import { Config } from "../../shared/domain/config";
import { debounce } from "lodash";
import { useResizeObserver } from "../hooks/resizeObserver";

const DEBOUNCE_INTERVAL_MS = 250;

Expand Down Expand Up @@ -151,6 +152,12 @@ export function Monaco(props: MonacoProps): JSX.Element {
[store.state],
);

// Monaco doesn't automatically resize when it's container element does so
// we need to listen for changes and trigger the refresh ourselves.
useResizeObserver(containerElement.current, () =>
monacoEditor.current?.layout(),
);

// Mount / Unmount
useEffect(() => {
// dragenter and dragover have to be cancelled in order for the drop event
Expand All @@ -165,7 +172,6 @@ export function Monaco(props: MonacoProps): JSX.Element {
};

const { current: el } = containerElement;
let resizeObserver: ResizeObserver | null = null;

if (el != null) {
monacoEditor.current = monaco.editor.create(el, {
Expand All @@ -179,16 +185,6 @@ export function Monaco(props: MonacoProps): JSX.Element {
disableKeybinding(monacoEditor.current, "editor.action.triggerSuggest");
}

// Monaco doesn't automatically resize when it's container element does so
// we need to listen for changes and trigger the refresh ourselves.

resizeObserver = new ResizeObserver(() => {
if (monacoEditor.current != null) {
monacoEditor.current.layout();
}
});
resizeObserver.observe(el);

el.addEventListener("dragenter", dragEnter);
el.addEventListener("dragover", dragOver);
el.addEventListener("drop", importAttachments);
Expand All @@ -200,8 +196,6 @@ export function Monaco(props: MonacoProps): JSX.Element {
onViewStateChange.flush();
onModelChange.flush();

resizeObserver?.disconnect();

if (monacoEditor.current != null) {
monacoEditor.current.dispose();
monacoEditor.current = null;
Expand Down
94 changes: 51 additions & 43 deletions packages/marqus-desktop/src/renderer/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useEffect, useMemo } from "react";
import React, { useEffect, useMemo, useRef } from "react";
import { Resizable } from "./shared/Resizable";
import { Focusable } from "./shared/Focusable";
import { Store, StoreContext, Listener } from "../store";
import styled from "styled-components";
import { h100, mb3, THEME, w100 } from "../css";
import { clamp, Dictionary, head, isEmpty, keyBy, round, take } from "lodash";
import { clamp, Dictionary, head, isEmpty, keyBy, take } from "lodash";
import {
Note,
getNoteById,
Expand All @@ -26,26 +26,66 @@ import { Section } from "../../shared/ui/app";
import { deleteNoteIfConfirmed } from "../utils/deleteNoteIfConfirmed";
import { cleanupClosedTabsCache, openTabsForNotes } from "./EditorToolbar";
import { remToPx, stripUnit } from "polished";
import { Size } from "../hooks/resizeObserver";
import { incrementScroll } from "../utils/dom";

const EXPANDED_ICON = faCaretDown;
const COLLAPSED_ICON = faCaretRight;
const MIN_WIDTH = "200px";

export interface SidebarProps {
store: Store;
}

export function Sidebar(props: SidebarProps): JSX.Element {
const { store } = props;
const { state } = store;
const { input } = state.sidebar;
const { notes } = state;
const expandedLookup = keyBy(state.sidebar.expanded, e => e);
const selectedLookup = keyBy(state.sidebar.selected, s => s);
const { input, expanded, selected } = state.sidebar;

const [menus, noteIds] = useMemo(
() => renderMenus(notes, store, input, expandedLookup, selectedLookup),
[notes, store, input, expandedLookup, selectedLookup],
);
const [menus, noteIds] = useMemo(() => {
const expandedLookup = keyBy(expanded, e => e);
const selectedLookup = keyBy(selected, s => s);

return renderMenus(notes, store, input, expandedLookup, selectedLookup);
}, [notes, store, input, expanded, selected]);

const maxScroll = useRef(0);
const onSidebarHeightChange = (size: Size) => {
maxScroll.current = size.scrollHeight;
};

const scrollUp: Listener<"sidebar.scrollUp"> = (_, { setUI }) => {
setUI(prev => {
const menuHeightInPx = remToPx(SIDEBAR_MENU_HEIGHT);
const menuHeightInt = stripUnit(menuHeightInPx) as number;

return {
sidebar: {
scroll: incrementScroll(prev.sidebar.scroll, -menuHeightInt, {
max: maxScroll.current,
roundBy: menuHeightInt,
}),
},
};
});
};

const scrollDown: Listener<"sidebar.scrollDown"> = (_, { setUI }) => {
setUI(prev => {
const menuHeightInPx = remToPx(SIDEBAR_MENU_HEIGHT);
const menuHeightInt = stripUnit(menuHeightInPx) as number;

return {
sidebar: {
scroll: incrementScroll(prev.sidebar.scroll, menuHeightInt, {
max: maxScroll.current,
roundBy: menuHeightInt,
}),
},
};
});
};

useEffect(() => {
const getNext = (increment: number) => {
Expand Down Expand Up @@ -183,6 +223,7 @@ export function Sidebar(props: SidebarProps): JSX.Element {
onScroll={async s => {
await store.dispatch("sidebar.updateScroll", s);
}}
onSizeChange={onSidebarHeightChange}
>
{menus}

Expand Down Expand Up @@ -377,39 +418,6 @@ export const updateScroll: Listener<"sidebar.updateScroll"> = (
});
};

export const scrollUp: Listener<"sidebar.scrollUp"> = (_, { setUI }) => {
setUI(prev => {
const menuHeightInPx = remToPx(SIDEBAR_MENU_HEIGHT);
const menuHeightInt = stripUnit(menuHeightInPx) as number;

const newScroll = prev.sidebar.scroll - menuHeightInt;
const roundedScroll = newScroll - (newScroll % menuHeightInt);
const clampedScroll = Math.max(roundedScroll, 0);

return {
sidebar: {
scroll: clampedScroll,
},
};
});
};

export const scrollDown: Listener<"sidebar.scrollDown"> = (_, { setUI }) => {
setUI(prev => {
const menuHeightInPx = remToPx(SIDEBAR_MENU_HEIGHT);
const menuHeightInt = stripUnit(menuHeightInPx) as number;

const newScroll = prev.sidebar.scroll + menuHeightInt;
const roundedScroll = newScroll - (newScroll % menuHeightInt);

return {
sidebar: {
scroll: roundedScroll,
},
};
});
};

export const toggleNoteExpanded: Listener<
"sidebar.toggleNoteExpanded" | "sidebar.toggleSelectedNoteExpanded"
> = (ev, ctx) => {
Expand Down Expand Up @@ -747,7 +755,7 @@ export const openSelectedNotes: Listener<"sidebar.openSelectedNotes"> = async (
return;
}

openTabsForNotes(ctx, selected);
openTabsForNotes(ctx, selected, selected[0]);

// Editor is not set as focused when a note is opened from the sidebar because
// the user may not want to start editing the note yet. This makes it easier
Expand Down
38 changes: 33 additions & 5 deletions packages/marqus-desktop/src/renderer/components/SidebarMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import styled from "styled-components";
import { PromisedInput } from "../../shared/promisedInput";
import { KeyCode, parseKeyCode } from "../../shared/io/keyCode";
import { isBlank } from "../../shared/utils";
import { Store } from "../store";
import { mt1, p2, px2, py1, THEME, w100 } from "../css";
import { px2, py1, THEME, w100 } from "../css";
import { Focusable, wasInsideFocusable } from "./shared/Focusable";
import { Icon, IconProps } from "./shared/Icon";
import { MouseDrag, useMouseDrag } from "../io/mouse";
import { Section } from "../../shared/ui/app";
import { partial } from "lodash";
import { getClosestAttribute } from "../utils/dom";
import { getClosestAttribute, isScrolledIntoView } from "../utils/dom";
import { createPortal } from "react-dom";
import { math } from "polished";

Expand Down Expand Up @@ -101,10 +107,10 @@ export function SidebarMenu(props: SidebarMenuProps): JSX.Element {
// Drag was inside sidebar, but not on a note. Move note to root.
else if (wasInsideFocusable(drag.event, Section.Sidebar)) {
onDrag();
} else {
// Drags that end outside of the sidebar should be considered cancels.
}

// Drags that end outside of the sidebar should be considered cancels.

setCursorEl(undefined);
}
},
Expand All @@ -123,6 +129,28 @@ export function SidebarMenu(props: SidebarMenuProps): JSX.Element {
}
}, [scrollToNoteId, id]);

// Scroll to menu if it's selected and offscreen
const prevIsSelectedRef = useRef(isSelected);
useLayoutEffect(() => {
const prevIsSelected = prevIsSelectedRef.current;
prevIsSelectedRef.current = isSelected;

// We only scroll to the menu when it goes from not-selected -> selected.
if (prevIsSelected || !isSelected) {
return;
}

const visCheck = isScrolledIntoView(menuRef.current);
if (visCheck.fullyVisible) {
return;
}

void store.dispatch(
"sidebar.updateScroll",
store.state.sidebar.scroll + visCheck.offBy,
);
}, [isSelected, store]);

return (
<>
<SidebarRow
Expand Down
Loading