mobile: migrate share extension

This commit is contained in:
Ammar Ahmed
2023-11-30 11:21:12 +05:00
committed by Abdullah Atta
parent c1b7c66e81
commit 1f94c47880
8 changed files with 859 additions and 650 deletions

View File

@@ -28,31 +28,35 @@ import { eClearEditor } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import { PressableButton } from "../ui/pressable";
export default function Notebooks({ note, close, full }) {
const { colors } = useThemeColors();
async function getNotebooks(item) {
let filteredNotebooks = [];
const relations = await db.relations.to(note, "notebook").resolve();
filteredNotebooks.push(relations);
if (!item.notebooks || item.notebooks.length < 1) return filteredNotebooks;
return filteredNotebooks;
async function getNotebooks() {
let filteredNotebooks = await db.relations.to(note, "notebook").resolve();
return filteredNotebooks || [];
}
const [noteNotebooks, setNoteNotebooks] = useState([]);
useEffect(() => {
getNotebooks().then((notebooks) => setNoteNotebooks(notebooks));
});
const navigateNotebook = (id) => {
let item = db.notebooks.notebook(id)?.data;
const navigateNotebook = async (id) => {
let item = await db.notebooks.notebook(id);
if (!item) return;
NotebookScreen.navigate(item, true);
};
const renderItem = (item) => (
<View
<PressableButton
key={item.id}
style={{
onPress={() => {
navigateNotebook(item.id);
eSendEvent(eClearEditor);
close();
}}
type={full ? "transparent" : "grayBg"}
customStyle={{
justifyContent: "flex-start",
paddingHorizontal: 12,
flexDirection: "row",
@@ -60,10 +64,7 @@ export default function Notebooks({ note, close, full }) {
flexShrink: 1,
flexGrow: 1,
padding: 6,
borderWidth: full ? 0 : 1,
borderColor: colors.primary.background,
borderRadius: 10,
backgroundColor: full ? "transparent" : colors.secondary.background,
minHeight: 42
}}
>
@@ -81,15 +82,10 @@ export default function Notebooks({ note, close, full }) {
maxWidth: "50%"
}}
size={SIZE.sm}
onPress={() => {
navigateNotebook(item.id);
eSendEvent(eClearEditor);
close();
}}
>
{item.title}
</Heading>
</View>
</PressableButton>
);
return noteNotebooks.length === 0 ? null : (
@@ -106,7 +102,7 @@ export default function Notebooks({ note, close, full }) {
{noteNotebooks.length > 1 && !full ? (
<Button
title={`See all linked notebooks`}
title={`View all linked notebooks`}
fontSize={SIZE.xs}
style={{
alignSelf: "flex-end",

View File

@@ -70,34 +70,26 @@ async function createNotes(bundle) {
if (!bundle.notebooks || !bundle.notebooks.length) {
const defaultNotebook = db.settings?.getDefaultNotebook();
if (defaultNotebook) {
if (!defaultNotebook.topic) {
db.relations.add(
{ type: "notebook", id: defaultNotebook.id },
{ id, type: "note" }
);
} else {
db.notes.addToNotebook(
{
id: defaultNotebook?.id,
topic: defaultNotebook.topic
},
id
);
}
await db.notes.addToNotebook(defaultNotebook, id);
}
} else {
for (const item of bundle.notebooks) {
if (item.type === "notebook") {
db.relations.add(item, { id, type: "note" });
} else {
db.notes.addToNotebook(
{
id: item.notebookId,
topic: item.id
},
id
);
}
await db.notes.addToNotebook(item, id);
}
}
if (bundle.tags) {
for (const tagId of bundle.tags) {
await db.relations.add(
{
type: "tag",
id: tagId
},
{
id: id,
type: "note"
}
);
}
}

View File

@@ -91,6 +91,8 @@ describe("VAULT", () => {
await elementById(notesnook.ids.dialogs.vault.pwd).typeText("1234");
await elementById(notesnook.ids.dialogs.vault.changePwd).typeText("2362");
await tapByText("Change");
await sleep(2000);
await device.pressBack();
await device.pressBack();
await device.pressBack();
await openLockedNote("2362");

View File

@@ -0,0 +1,139 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Text, TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useDBItem } from "../app/hooks/use-db-item";
import { useShareStore } from "./store";
export const AddNotebooks = ({ onPress }) => {
const { colors } = useThemeColors();
const notebookIds = useShareStore((state) => state.selectedNotebooks);
return (
<TouchableOpacity
onPress={onPress}
activeOpacity={0.7}
style={{
paddingHorizontal: 12,
width: "100%",
marginRight: 0,
borderWidth: 1,
borderColor: colors.secondary.background,
justifyContent: "center",
borderRadius: 5,
marginBottom: 10,
paddingVertical: 10,
paddingTop: 5
}}
>
{!notebookIds || !notebookIds.length ? (
<>
<View
style={{
width: "100%",
flexDirection: "row",
marginTop: 5
}}
>
<Icon
name="notebook-outline"
size={20}
style={{
marginRight: 10
}}
color={colors.secondary.icon}
/>
<Text
style={{
color: colors.secondary.icon,
fontSize: 15
}}
>
Add to notebook
</Text>
</View>
</>
) : (
<View
style={{
flexWrap: "wrap",
width: "100%",
flexDirection: "row"
}}
>
{notebookIds.map((id) => (
<NotebookItem key={id} id={id} />
))}
<Text
style={{
color: colors.primary.accent,
marginRight: 5,
fontSize: 14,
borderRadius: 4,
paddingHorizontal: 8,
backgroundColor: colors.secondary.background,
paddingVertical: 5,
marginTop: 5
}}
onPress={() => {
onPress();
}}
key="$add-more"
>
<Icon name="plus" size={16} /> Add more
</Text>
</View>
)}
</TouchableOpacity>
);
};
const NotebookItem = ({ id }) => {
const { colors } = useThemeColors();
const [item] = useDBItem(id, "notebook");
const notebookIds = useShareStore((state) => state.selectedNotebooks);
return !item ? null : (
<Text
style={{
color: colors.secondary.icon,
marginRight: 5,
fontSize: 14,
borderRadius: 4,
paddingHorizontal: 8,
backgroundColor: colors.secondary.background,
paddingVertical: 5,
marginTop: 5
}}
onPress={() => {
const index = notebookIds.indexOf(id);
const selectedNotebooks = [...notebookIds];
selectedNotebooks.splice(index, 1);
useShareStore.getState().setSelectedNotebooks(selectedNotebooks);
}}
key={item.id}
>
<Icon name={"notebook-outline"} size={15} /> {item.title}
</Text>
);
};

View File

@@ -0,0 +1,133 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { useThemeColors } from "@notesnook/theme";
import { Text, TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useDBItem } from "../app/hooks/use-db-item";
import { useShareStore } from "./store";
export const AddTags = ({ onPress }) => {
const { colors } = useThemeColors();
const tagIds = useShareStore((state) => state.selectedTags);
return (
<TouchableOpacity
onPress={onPress}
activeOpacity={0.7}
style={{
paddingHorizontal: 12,
minHeight: 45,
width: "100%",
marginRight: 0,
borderWidth: 1,
borderColor: colors.secondary.background,
justifyContent: "center",
borderRadius: 5,
marginBottom: 10
}}
>
{!tagIds || !tagIds.length ? (
<>
<View
style={{
width: "100%",
flexDirection: "row"
}}
>
<Icon
name="pound"
size={20}
style={{
marginRight: 10
}}
color={colors.secondary.icon}
/>
<Text
style={{
color: colors.secondary.icon,
fontSize: 15
}}
>
Add tags
</Text>
</View>
</>
) : (
<View
style={{
flexWrap: "wrap",
width: "100%",
flexDirection: "row"
}}
>
{tagIds.map((id) => (
<TagItem key={id} tagId={id} />
))}
<Text
style={{
color: colors.primary.accent,
marginRight: 5,
fontSize: 14,
borderRadius: 4,
paddingHorizontal: 8,
backgroundColor: colors.secondary.background,
paddingVertical: 5
}}
onPress={() => {
onPress();
}}
key="$add-tag"
>
<Icon name="plus" size={17} />
Add tag
</Text>
</View>
)}
</TouchableOpacity>
);
};
const TagItem = ({ tagId }) => {
const { colors } = useThemeColors();
const [item] = useDBItem(tagId, "tag");
const tagIds = useShareStore((state) => state.selectedTags);
return !item ? null : (
<Text
style={{
color: colors.secondary.icon,
marginRight: 5,
fontSize: 14,
borderRadius: 4,
paddingHorizontal: 8,
backgroundColor: colors.secondary.background,
paddingVertical: 5
}}
onPress={() => {
const index = tagIds.indexOf(tagId);
const selectedTags = [...tagId];
selectedTags.splice(index, 1);
useShareStore.getState().setSelectedTags(selectedTags);
}}
>
#{item.title}
</Text>
);
};

View File

@@ -1,391 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef, useState } from "react";
import {
Platform,
StatusBar,
Text,
TextInput,
TouchableOpacity,
View,
useWindowDimensions,
FlatList
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../app/common/database";
import { getElevationStyle } from "../app/utils/elevation";
import { initDatabase, useShareStore } from "./store";
import { useThemeColors } from "@notesnook/theme";
import { groupArray } from "@notesnook/core/dist/utils/grouping";
const ListItem = ({ item, mode, close }) => {
const { colors } = useThemeColors();
const selectedNotebooks = useShareStore((state) => state.selectedNotebooks);
const selectedTags = useShareStore((state) => state.selectedTags);
const isSelected =
mode === "appendNote"
? false
: mode === "selectTags"
? selectedTags.indexOf(item.alias) > -1
: selectedNotebooks.findIndex(
(selected) => selected.id === item.id && selected.type === item.type
) > -1;
const SearchSetters = React.useMemo(
() => ({
appendNote: (item) => {
useShareStore.getState().setAppendNote(item);
close();
},
selectNotebooks: (item) => {
const selectedNotebooks = [
...useShareStore.getState().selectedNotebooks
];
const currentIndex = selectedNotebooks.findIndex(
(selected) => selected.id === item.id && selected.type === item.type
);
if (currentIndex === -1) {
selectedNotebooks.push(item);
} else {
selectedNotebooks.splice(currentIndex, 1);
}
useShareStore.getState().setSelectedNotebooks(selectedNotebooks);
},
selectTags: (item) => {
const selectedTags = [...useShareStore.getState().selectedTags];
const currentIndex = selectedTags.indexOf(item.alias);
if (currentIndex === -1) {
selectedTags.push(item.alias);
} else {
selectedTags.splice(currentIndex, 1);
}
useShareStore.getState().setSelectedTags(selectedTags);
}
}),
[close]
);
const set = React.useMemo(() => SearchSetters[mode], [mode, SearchSetters]);
const onSelectItem = async (item) => {
if (item.locked) {
return;
}
set(item);
};
return (
<TouchableOpacity
activeOpacity={0.7}
onPress={() => onSelectItem(item)}
style={{
flexDirection: "column",
borderBottomWidth: item.topics?.length > 0 ? 0 : 1,
borderBottomColor: colors.primary.border,
justifyContent: "center",
paddingVertical: 12
}}
>
<View
style={{
flexDirection: "row",
paddingHorizontal: 12,
paddingLeft: item.type === "topic" ? 0 : 12
}}
>
{item.type !== "note" ? (
<Icon
name={
!isSelected
? "checkbox-blank-circle-outline"
: "check-circle-outline"
}
style={{
marginRight: 10
}}
size={20}
color={isSelected ? colors.primary.accent : colors.secondary.icon}
/>
) : null}
<View
style={{
flexDirection: "column"
}}
>
<Text
numberOfLines={1}
style={{
color: colors.primary.paragraph,
fontWeight: item.type === "topic" ? "normal" : "bold",
fontSize: 15
}}
>
{item.type === "tag" ? "#" : ""}
{item.alias || item.title}
</Text>
</View>
</View>
{item.type === "notebook" && item.topics && item.topics.length > 0 ? (
<View
style={{
paddingLeft: 40,
marginTop: 5
}}
>
{item.topics.map((topic) => (
<ListItem key={topic.id} item={topic} mode={mode} close={close} />
))}
</View>
) : null}
</TouchableOpacity>
);
};
const SearchGetters = {
appendNote: () =>
groupArray([...db.notes.all], db.settings.getGroupOptions("notes")).filter(
(item) => item.type === "note"
),
selectNotebooks: () =>
groupArray(
[...db.notebooks.all],
db.settings.getGroupOptions("notebooks")
).filter((item) => item.type === "notebook"),
selectTags: () => {
const selected = useShareStore.getState().selectedTags;
const tags = [];
if (selected)
tags.push(
...selected.map((t) => ({
alias: t,
type: "tag"
}))
);
for (let tag of db.tags.all) {
const index = tags.findIndex((t) => t.alias === tag.alias);
// Skip selected tags as they are already in the list.
if (index > -1) continue;
tags.push(tag);
}
return groupArray([...tags], {
groupBy: "none",
sortDirection: "asc",
sortBy: "title"
}).filter((item) => item.type === "tag");
}
};
const SearchLookup = {
appendNote: (items, kwd) => {
return db.lookup.notes(items, kwd);
},
selectNotebooks: (items, kwd) => {
return db.lookup.notebooks(items, kwd);
},
selectTags: (items, kwd) => {
return db.lookup.tags(items, kwd);
}
};
const SearchPlaceholder = {
appendNote: "Search for a note",
selectNotebooks: "Search for a notebook",
selectTags: "Search for a tag"
};
export const Search = ({ close, getKeyboardHeight, quicknote, mode }) => {
const { colors } = useThemeColors();
const { height } = useWindowDimensions();
const timer = useRef(null);
const inputRef = useRef();
const [searchResults, setSearchResults] = useState([]);
const searchableItems = useRef(null);
const searchKeyword = useRef(null);
const insets =
Platform.OS === "android"
? { top: StatusBar.currentHeight }
: // eslint-disable-next-line react-hooks/rules-of-hooks
useSafeAreaInsets();
const get = SearchGetters[mode];
const lookup = SearchLookup[mode];
const onSearch = async () => {
if (!searchableItems.current) {
await initDatabase();
searchableItems.current = get();
}
if (timer.current) {
clearTimeout(timer.current);
timer.current = null;
}
timer.current = setTimeout(async () => {
if (!searchKeyword.current) {
setSearchResults([]);
setSearchResults(get());
return;
}
setSearchResults(
await lookup(searchableItems.current, searchKeyword.current)
);
}, 150);
};
useEffect(() => {
(async () => {
await initDatabase();
searchableItems.current = get();
setSearchResults(searchableItems.current);
})();
}, [get]);
const renderItem = ({ item }) =>
!item.locked ? <ListItem item={item} mode={mode} close={close} /> : null;
let extra = quicknote
? {
marginTop: -insets.top,
paddingTop: insets.top
}
: {};
const searchHeight = height - getKeyboardHeight();
return (
<View
style={{
position: "absolute",
top: Platform.OS === "android" ? 20 : 0,
backgroundColor: colors.secondary.background,
borderRadius: quicknote ? 0 : 10,
width: quicknote ? "100%" : "95%",
minHeight: 250,
alignSelf: "center",
overflow: "hidden",
zIndex: 999,
...getElevationStyle(quicknote ? 1 : 5),
...extra
}}
>
<View
style={{
flexShrink: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 12,
marginBottom: 10,
height: 50
}}
>
<Icon
name="arrow-left"
color={colors.secondary.icon}
style={{
marginRight: 10
}}
size={25}
onPress={close}
/>
<TextInput
ref={inputRef}
placeholder={SearchPlaceholder[mode]}
placeholderTextColor={colors.primary.placeholder}
style={{
fontSize: 15,
flex: 1
}}
onChangeText={(value) => {
if (mode === "selectTags") {
searchKeyword.current = db.tags.sanitize(value);
} else {
searchKeyword.current = value;
}
onSearch();
}}
onSubmitEditing={onSearch}
/>
<Icon
name="magnify"
color={colors.primary.icon}
size={25}
onPress={onSearch}
/>
</View>
<View
style={{
maxHeight: searchHeight > 550 ? 550 : searchHeight,
height: searchHeight > 550 ? 550 : searchHeight
}}
>
<FlatList
data={searchResults}
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
renderItem={renderItem}
ListHeaderComponent={
mode === "selectTags" &&
searchKeyword.current &&
searchKeyword.current.length > 0 &&
!searchResults.find(
(item) => item.title === searchKeyword.current
) ? (
<ListItem
item={{
type: "tag",
alias: searchKeyword.current
}}
mode={mode}
close={close}
/>
) : null
}
ListFooterComponent={<View style={{ height: 200 }} />}
ListEmptyComponent={
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
height: 200
}}
>
<Text
style={{
color: colors.secondary.icon
}}
>
{searchKeyword.current
? `No results found for "${searchKeyword.current}"`
: "Search for a note to append to it."}
</Text>
</View>
}
/>
</View>
</View>
);
};

View File

@@ -0,0 +1,548 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Item, Note, Notebook, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import {
FlatList,
Platform,
StatusBar,
Text,
TextInput,
TouchableOpacity,
View,
useWindowDimensions
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../app/common/database";
import { useDBItem } from "../app/hooks/use-db-item";
import { useNotebook } from "../app/hooks/use-notebook";
import { getElevationStyle } from "../app/utils/elevation";
import { initDatabase, useShareStore } from "./store";
import { createItemSelectionStore } from "../app/stores/item-selection-store";
import create from "zustand";
export const useNotebookExpandedStore = create<{
expanded: {
[id: string]: boolean;
};
setExpanded: (id: string) => void;
}>((set, get) => ({
expanded: {},
setExpanded(id: string) {
set({
expanded: {
...get().expanded,
[id]: !get().expanded[id]
}
});
}
}));
const SearchSetters = {
appendNote: (id: string) => {
useShareStore.getState().setAppendNote(id);
close();
},
selectNotebooks: (id: string) => {
const selectedNotebooks = [...useShareStore.getState().selectedNotebooks];
const currentIndex = selectedNotebooks.findIndex(
(selected) => id === selected
);
if (currentIndex === -1) {
selectedNotebooks.push(id as never);
} else {
selectedNotebooks.splice(currentIndex, 1);
}
useShareStore.getState().setSelectedNotebooks(selectedNotebooks);
},
selectTags: (id: string) => {
const selectedTags = [...useShareStore.getState().selectedTags];
const currentIndex = selectedTags.indexOf(id as never);
if (currentIndex === -1) {
selectedTags.push(id as never);
} else {
selectedTags.splice(currentIndex, 1);
}
useShareStore.getState().setSelectedTags(selectedTags);
}
};
const SearchGetters = {
appendNote: () => db.notes.all.sorted(db.settings.getGroupOptions("notes")),
selectNotebooks: () =>
db.notebooks.roots.sorted(db.settings.getGroupOptions("notebooks")),
selectTags: () => {
return db.tags.all.sorted(db.settings.getGroupOptions("tags"));
}
};
const SearchLookup = {
appendNote: (query: string) => {
return db.lookup.notes(query).sorted();
},
selectNotebooks: (query: string) => {
return db.lookup.notebooks(query).sorted();
},
selectTags: (query: string) => {
return db.lookup.tags(query).sorted();
}
};
const SearchPlaceholder = {
appendNote: "Search for a note",
selectNotebooks: "Search for a notebook",
selectTags: "Search for a tag"
};
type SearchMode = "appendNote" | "selectNotebooks" | "selectTags";
const NotebookItem = ({
id,
mode,
close,
items,
level = 0
}: {
id: string;
mode: SearchMode;
close?: () => void;
level?: number;
items?: VirtualizedGrouping<Notebook>;
}) => {
const isExpanded = useNotebookExpandedStore((state) => state.expanded[id]);
const { nestedNotebooks, notebook } = useNotebook(id, items);
const { colors } = useThemeColors();
const isSelected = useShareStore(
(state) =>
state.selectedNotebooks.findIndex((selectedId) => id === selectedId) > -1
);
const set = SearchSetters[mode];
const onSelectItem = async () => {
set(id);
};
return !notebook ? (
<View
style={{
height: 40
}}
/>
) : (
<TouchableOpacity
activeOpacity={0.7}
onPress={() => onSelectItem()}
style={{
flexDirection: "column",
justifyContent: "center"
}}
>
<View
style={{
flexDirection: "row",
paddingHorizontal: 12,
height: 40,
alignItems: "center"
}}
>
{nestedNotebooks?.ids.length ? (
<TouchableOpacity
style={{
width: 35,
height: 35,
justifyContent: "center",
alignItems: "center"
}}
onPress={() => {
useNotebookExpandedStore.getState().setExpanded(id);
}}
activeOpacity={1}
>
<Icon
size={20}
color={colors.primary.icon}
name={isExpanded ? "chevron-down" : "chevron-right"}
/>
</TouchableOpacity>
) : (
<View
style={{
width: 35,
height: 30,
justifyContent: "center",
alignItems: "center"
}}
/>
)}
<Icon
name={
!isSelected
? "checkbox-blank-circle-outline"
: "check-circle-outline"
}
style={{
marginRight: 10
}}
size={20}
color={isSelected ? colors.primary.accent : colors.secondary.icon}
/>
<View
style={{
flexDirection: "column"
}}
>
<Text
numberOfLines={1}
style={{
color: colors.primary.paragraph,
fontSize: 15
}}
>
{notebook.title}
</Text>
</View>
</View>
{nestedNotebooks?.ids?.length && isExpanded ? (
<View
style={{
paddingLeft: level + 1 > 0 && level + 1 < 5 ? 15 : 0,
marginTop: 5
}}
>
{(nestedNotebooks.ids as string[]).map((id) => (
<NotebookItem
key={id}
id={id}
mode={mode}
close={close}
level={level + 1}
/>
))}
</View>
) : null}
</TouchableOpacity>
);
};
const ListItem = ({
id,
mode,
close,
items
}: {
id: string;
mode: SearchMode;
close?: () => void;
items?: VirtualizedGrouping<Item>;
}) => {
const [item] = useDBItem(
id,
mode === "appendNote" ? "note" : "tag",
items as any
);
const { colors } = useThemeColors();
const isSelected = useShareStore((state) =>
mode === "appendNote" ? false : state.selectedTags.indexOf(id as never) > -1
);
const set = SearchSetters[mode];
const onSelectItem = async () => {
if ((item as Note)?.locked) {
return;
}
set(id);
};
return !item ? null : (
<TouchableOpacity
activeOpacity={0.7}
onPress={() => onSelectItem()}
style={{
flexDirection: "column",
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
justifyContent: "center",
paddingVertical: 12
}}
>
<View
style={{
flexDirection: "row",
paddingHorizontal: 12
}}
>
{item.type !== "note" ? (
<Icon
name={
!isSelected
? "checkbox-blank-circle-outline"
: "check-circle-outline"
}
style={{
marginRight: 10
}}
size={20}
color={isSelected ? colors.primary.accent : colors.secondary.icon}
/>
) : null}
<View
style={{
flexDirection: "column"
}}
>
<Text
numberOfLines={1}
style={{
color: colors.primary.paragraph,
fontSize: 15
}}
>
{item.type === "tag" ? "#" : ""}
{item.title}
</Text>
</View>
</View>
</TouchableOpacity>
);
};
export const Search = ({
close,
getKeyboardHeight,
quicknote,
mode
}: {
close?: () => void;
getKeyboardHeight: () => number;
quicknote?: boolean;
mode: SearchMode;
}) => {
const { colors } = useThemeColors();
const { height } = useWindowDimensions();
const timer = useRef<NodeJS.Timeout>();
const inputRef = useRef<TextInput>(null);
const [items, setItems] = useState<VirtualizedGrouping<Item>>();
const [searchKeyword, setSearchKeyword] = useState<string>();
const [queryExists, setQueryExists] = useState(false);
const checkQueryExists = (query: string) => {
if (!query) {
setQueryExists(false);
}
db.tags.all
.find((v) => v.and([v(`title`, "==", query)]))
.then((exists) => setQueryExists(!!exists));
};
const insets =
Platform.OS === "android"
? { top: StatusBar.currentHeight }
: // eslint-disable-next-line react-hooks/rules-of-hooks
useSafeAreaInsets();
const get = SearchGetters[mode];
const lookup = SearchLookup[mode];
const onSearch = async () => {
if (timer.current) {
clearTimeout(timer.current);
timer.current = undefined;
}
timer.current = setTimeout(async () => {
if (!searchKeyword) {
setItems(undefined);
setItems(await get());
setQueryExists(false);
return;
}
setItems(await lookup(searchKeyword));
}, 150);
};
useEffect(() => {
(async () => {
await initDatabase();
setItems(await get());
})();
}, [get]);
const renderItem = React.useCallback(
({ item }: { item: string }) =>
mode === "selectNotebooks" ? (
<NotebookItem
id={item}
mode={mode}
close={close}
items={items as any}
/>
) : (
<ListItem id={item} mode={mode} close={close} items={items} />
),
[close, mode, items]
);
const customStyles = quicknote
? {
marginTop: -(insets.top || 0),
paddingTop: insets.top
}
: {};
const searchHeight = height - getKeyboardHeight();
return (
<View
style={{
position: "absolute",
top: Platform.OS === "android" ? 20 : 0,
backgroundColor: colors.primary.background,
borderRadius: quicknote ? 0 : 10,
width: quicknote ? "100%" : "95%",
minHeight: 250,
alignSelf: "center",
overflow: "hidden",
zIndex: 999,
...getElevationStyle(quicknote ? 1 : 5),
...customStyles
}}
>
<View
style={{
flexShrink: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 16,
paddingTop: 6,
marginBottom: 10,
height: 50
}}
>
<Icon
name="arrow-left"
color={colors.secondary.icon}
style={{
marginRight: 10
}}
size={25}
onPress={close}
/>
<TextInput
ref={inputRef}
placeholder={SearchPlaceholder[mode]}
placeholderTextColor={colors.primary.placeholder}
style={{
fontSize: 16,
flex: 1
}}
onChangeText={(value) => {
setSearchKeyword(value);
onSearch();
checkQueryExists(value);
}}
onSubmitEditing={onSearch}
/>
<Icon
name="magnify"
color={colors.primary.icon}
size={25}
onPress={onSearch}
/>
</View>
<View
style={{
maxHeight: searchHeight > 550 ? 550 : searchHeight,
height: searchHeight > 550 ? 550 : searchHeight
}}
>
{mode === "selectTags" && !queryExists && searchKeyword ? (
<TouchableOpacity
style={{
flexDirection: "row",
paddingHorizontal: 12,
height: 40,
alignItems: "center"
}}
activeOpacity={1}
onPress={async () => {
if (!searchKeyword) return;
const tagId = await db.tags.add({
title: searchKeyword
});
SearchSetters.selectTags(tagId);
onSearch();
checkQueryExists(searchKeyword);
}}
>
<Text
numberOfLines={1}
style={{
color: colors.primary.paragraph,
fontSize: 15
}}
>
Add #{searchKeyword}
</Text>
</TouchableOpacity>
) : null}
<FlatList
data={items?.ids as string[]}
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
renderItem={renderItem}
windowSize={1}
ListFooterComponent={<View style={{ height: 200 }} />}
ListEmptyComponent={
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
height: 200
}}
>
<Text
style={{
color: colors.secondary.icon
}}
>
{searchKeyword
? `No results found for "${searchKeyword}"`
: mode === "appendNote"
? "No notes"
: mode === "selectTags"
? "No tags"
: "No notebooks"}
</Text>
</View>
}
/>
</View>
</View>
);
};

View File

@@ -52,6 +52,8 @@ import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../app/utils/constants";
import { getElevationStyle } from "../app/utils/elevation";
import { eOnLoadNote } from "../app/utils/events";
import { NoteBundle } from "../app/utils/note-bundle";
import { AddNotebooks } from "./add-notebooks";
import { AddTags } from "./add-tags";
import { Editor } from "./editor";
import { HtmlLoadingWebViewAgent, fetchHandle } from "./fetch-webview";
import { Search } from "./search";
@@ -281,8 +283,6 @@ const ShareView = ({ quicknote = false }) => {
};
} else {
_note = { ...note };
_note.tags = useShareStore.getState().selectedTags || [];
_note.content.data = noteContent.current;
_note.sessionId = Date.now();
}
@@ -290,7 +290,8 @@ const ShareView = ({ quicknote = false }) => {
await NoteBundle.createNotes({
files: rawFiles,
note: _note,
notebooks: useShareStore.getState().selectedNotebooks
notebooks: useShareStore.getState().selectedNotebooks,
tags: useShareStore.getState().selectedTags
});
try {
@@ -840,217 +841,6 @@ const ShareView = ({ quicknote = false }) => {
);
};
const AddNotebooks = ({ onPress }) => {
const { colors } = useThemeColors();
const notebooks = useShareStore((state) => state.selectedNotebooks);
return (
<TouchableOpacity
onPress={onPress}
activeOpacity={0.7}
style={{
paddingHorizontal: 12,
width: "100%",
marginRight: 0,
borderWidth: 1,
borderColor: colors.secondary.background,
justifyContent: "center",
borderRadius: 5,
marginBottom: 10,
paddingVertical: 10,
paddingTop: 5
}}
>
{!notebooks || !notebooks.length ? (
<>
<View
style={{
width: "100%",
flexDirection: "row",
marginTop: 5
}}
>
<Icon
name="notebook-outline"
size={20}
style={{
marginRight: 10
}}
color={colors.secondary.icon}
/>
<Text
style={{
color: colors.secondary.icon,
fontSize: 15
}}
>
Add to notebook
</Text>
</View>
</>
) : (
<View
style={{
flexWrap: "wrap",
width: "100%",
flexDirection: "row"
}}
>
{notebooks.map((item) => (
<Text
style={{
color: colors.secondary.icon,
marginRight: 5,
fontSize: 14,
borderRadius: 4,
paddingHorizontal: 8,
backgroundColor: colors.secondary.background,
paddingVertical: 5,
marginTop: 5
}}
onPress={() => {
const index = notebooks.findIndex(
(nb) => nb.id === item.id && nb.type === item.type
);
const selectedNotebooks = [...notebooks];
selectedNotebooks.splice(index, 1);
useShareStore
.getState()
.setSelectedNotebooks(selectedNotebooks);
}}
key={item.id}
>
<Icon
name={item.type === "topic" ? "bookmark" : "notebook-outline"}
size={15}
/>{" "}
{item.title}
</Text>
))}
<Text
style={{
color: colors.primary.accent,
marginRight: 5,
fontSize: 14,
borderRadius: 4,
paddingHorizontal: 8,
backgroundColor: colors.secondary.background,
paddingVertical: 5,
marginTop: 5
}}
onPress={() => {
onPress();
}}
key="$add-more"
>
<Icon name="plus" size={16} /> Add more
</Text>
</View>
)}
</TouchableOpacity>
);
};
const AddTags = ({ onPress }) => {
const { colors } = useThemeColors();
const tags = useShareStore((state) => state.selectedTags);
return (
<TouchableOpacity
onPress={onPress}
activeOpacity={0.7}
style={{
paddingHorizontal: 12,
minHeight: 45,
width: "100%",
marginRight: 0,
borderWidth: 1,
borderColor: colors.secondary.background,
justifyContent: "center",
borderRadius: 5,
marginBottom: 10
}}
>
{!tags || !tags.length ? (
<>
<View
style={{
width: "100%",
flexDirection: "row"
}}
>
<Icon
name="pound"
size={20}
style={{
marginRight: 10
}}
color={colors.secondary.icon}
/>
<Text
style={{
color: colors.secondary.icon,
fontSize: 15
}}
>
Add tags
</Text>
</View>
</>
) : (
<View
style={{
flexWrap: "wrap",
width: "100%",
flexDirection: "row"
}}
>
{tags.map((tag) => (
<Text
style={{
color: colors.secondary.icon,
marginRight: 5,
fontSize: 14,
borderRadius: 4,
paddingHorizontal: 8,
backgroundColor: colors.secondary.background,
paddingVertical: 5
}}
onPress={() => {
const index = tags.indexOf(tag);
const selectedTags = [...tags];
selectedTags.splice(index, 1);
useShareStore.getState().setSelectedTags(selectedTags);
}}
key={tag}
>
#{tag}
</Text>
))}
<Text
style={{
color: colors.primary.accent,
marginRight: 5,
fontSize: 14,
borderRadius: 4,
paddingHorizontal: 8,
backgroundColor: colors.secondary.background,
paddingVertical: 5
}}
onPress={() => {
onPress();
}}
key="$add-tag"
>
<Icon name="plus" size={17} />
Add tag
</Text>
</View>
)}
</TouchableOpacity>
);
};
const Button = ({
title,
onPress,