Compare commits
1 Commits
fix-ipad-o
...
feature/ta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52d48352fd |
@@ -17,34 +17,70 @@ 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, { useRef, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import {
|
||||
eCloseAttachmentDialog,
|
||||
eOpenAttachmentsDialog
|
||||
} from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import DialogHeader from "../dialog/dialog-header";
|
||||
import { Toast } from "../toast";
|
||||
import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
import SheetWrapper from "../ui/sheet";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
|
||||
export const AttachmentDialog = ({ data }) => {
|
||||
export const AttachmentDialog = () => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [note, setNote] = useState(data);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [note, setNote] = useState(null);
|
||||
const actionSheetRef = useRef();
|
||||
const [attachments, setAttachments] = useState(
|
||||
data
|
||||
? db.attachments.ofNote(data.id, "all")
|
||||
: [...(db.attachments.all || [])]
|
||||
);
|
||||
const [attachments, setAttachments] = useState([]);
|
||||
const attachmentSearchValue = useRef();
|
||||
const searchTimer = useRef();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenAttachmentsDialog, open);
|
||||
eSubscribeEvent(eCloseAttachmentDialog, close);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenAttachmentsDialog, open);
|
||||
eUnSubscribeEvent(eCloseAttachmentDialog, close);
|
||||
};
|
||||
}, [visible]);
|
||||
|
||||
const open = (data) => {
|
||||
if (data?.id) {
|
||||
setNote(data);
|
||||
let _attachments = db.attachments.ofNote(data.id, "all");
|
||||
setAttachments(_attachments);
|
||||
} else {
|
||||
setAttachments([...db.attachments.all]);
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
actionSheetRef.current?.show();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const close = () => {
|
||||
actionSheetRef.current?.hide();
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const onChangeText = (text) => {
|
||||
attachmentSearchValue.current = text;
|
||||
if (
|
||||
@@ -68,104 +104,107 @@ export const AttachmentDialog = ({ data }) => {
|
||||
<AttachmentItem setAttachments={setAttachments} attachment={item} />
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12
|
||||
return !visible ? null : (
|
||||
<SheetWrapper
|
||||
centered={false}
|
||||
fwdRef={actionSheetRef}
|
||||
onClose={async () => {
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title={note ? "Attachments" : "Manage attachments"}
|
||||
paragraph="Tap on an attachment to view properties"
|
||||
button={{
|
||||
title: "Check all",
|
||||
type: "grayAccent",
|
||||
loading: loading,
|
||||
onPress: async () => {
|
||||
setLoading(true);
|
||||
for (let attachment of attachments) {
|
||||
let result = await filesystem.checkAttachment(
|
||||
attachment.metadata.hash
|
||||
);
|
||||
if (result.failed) {
|
||||
db.attachments.markAsFailed(
|
||||
attachment.metadata.hash,
|
||||
result.failed
|
||||
);
|
||||
} else {
|
||||
db.attachments.markAsFailed(attachment.id, null);
|
||||
}
|
||||
setAttachments([...db.attachments.all]);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Seperator />
|
||||
{!note ? (
|
||||
<Input
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<FlatList
|
||||
nestedScrollEnabled
|
||||
overScrollMode="never"
|
||||
scrollToOverflowEnabled={false}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
onMomentumScrollEnd={() => {
|
||||
actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 150,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon name="attachment" size={60} color={colors.icon} />
|
||||
<Paragraph>
|
||||
{note ? "No attachments on this note" : "No attachments"}
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 350
|
||||
}}
|
||||
/>
|
||||
}
|
||||
data={attachments}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}
|
||||
<Toast context="local" />
|
||||
<View
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 10
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Icon name="shield-key-outline" size={SIZE.xs} color={colors.icon} />
|
||||
{" "}All attachments are end-to-end encrypted.
|
||||
</Paragraph>
|
||||
</View>
|
||||
<DialogHeader
|
||||
title={note ? "Attachments" : "Manage attachments"}
|
||||
paragraph="Tap on an attachment to view properties"
|
||||
button={{
|
||||
title: "Check all",
|
||||
type: "grayAccent",
|
||||
loading: loading,
|
||||
onPress: async () => {
|
||||
setLoading(true);
|
||||
for (let attachment of attachments) {
|
||||
let result = await filesystem.checkAttachment(
|
||||
attachment.metadata.hash
|
||||
);
|
||||
if (result.failed) {
|
||||
db.attachments.markAsFailed(
|
||||
attachment.metadata.hash,
|
||||
result.failed
|
||||
);
|
||||
} else {
|
||||
db.attachments.markAsFailed(attachment.id, null);
|
||||
}
|
||||
setAttachments([...db.attachments.all]);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Seperator />
|
||||
{!note ? (
|
||||
<Input
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<FlatList
|
||||
nestedScrollEnabled
|
||||
overScrollMode="never"
|
||||
scrollToOverflowEnabled={false}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
onMomentumScrollEnd={() => {
|
||||
actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 150,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon name="attachment" size={60} color={colors.icon} />
|
||||
<Paragraph>
|
||||
{note ? "No attachments on this note" : "No attachments"}
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 350
|
||||
}}
|
||||
/>
|
||||
}
|
||||
data={attachments}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<Icon name="shield-key-outline" size={SIZE.xs} color={colors.icon} />
|
||||
{" "}All attachments are end-to-end encrypted.
|
||||
</Paragraph>
|
||||
</View>
|
||||
</SheetWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
AttachmentDialog.present = (note) => {
|
||||
presentSheet({
|
||||
component: () => <AttachmentDialog data={note} />
|
||||
});
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import React from "react";
|
||||
import { useNoteStore } from "../../stores/use-notes-store";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { AnnouncementDialog } from "../announcements";
|
||||
import { AttachmentDialog } from "../attachments";
|
||||
import AuthModal from "../auth/auth-modal";
|
||||
import { SessionExpired } from "../auth/session-expired";
|
||||
import { Dialog } from "../dialog";
|
||||
@@ -33,6 +34,10 @@ import MergeConflicts from "../merge-conflicts";
|
||||
import PremiumDialog from "../premium";
|
||||
import { Expiring } from "../premium/expiring";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { AddNotebookSheet } from "../sheets/add-notebook";
|
||||
import AddToNotebookSheet from "../sheets/add-to";
|
||||
import ManageTagsSheet from "../sheets/manage-tags";
|
||||
import PublishNoteSheet from "../sheets/publish-note";
|
||||
import RateAppSheet from "../sheets/rate-app";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
import RestoreDataSheet from "../sheets/restore-data";
|
||||
@@ -46,6 +51,7 @@ const DialogProvider = () => {
|
||||
<LoadingDialog />
|
||||
<Dialog context="global" />
|
||||
<AddTopicDialog colors={colors} />
|
||||
<AddNotebookSheet colors={colors} />
|
||||
<PremiumDialog colors={colors} />
|
||||
<AuthModal colors={colors} />
|
||||
<MergeConflicts />
|
||||
@@ -55,8 +61,12 @@ const DialogProvider = () => {
|
||||
<RestoreDataSheet />
|
||||
<ResultDialog />
|
||||
<VaultDialog colors={colors} />
|
||||
<AddToNotebookSheet colors={colors} />
|
||||
<RateAppSheet />
|
||||
<ImagePreview />
|
||||
<PublishNoteSheet />
|
||||
<ManageTagsSheet />
|
||||
<AttachmentDialog />
|
||||
{loading ? null : <Expiring />}
|
||||
<AnnouncementDialog />
|
||||
<SessionExpired />
|
||||
|
||||
@@ -35,7 +35,6 @@ import BaseDialog from "./base-dialog";
|
||||
import DialogButtons from "./dialog-buttons";
|
||||
import DialogHeader from "./dialog-header";
|
||||
import { useCallback } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export const Dialog = ({ context = "global" }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -55,11 +54,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
input: false,
|
||||
inputPlaceholder: "Enter some text",
|
||||
defaultValue: "",
|
||||
disableBackdropClosing: false,
|
||||
check: {
|
||||
info: "Check",
|
||||
type: "transparent"
|
||||
}
|
||||
disableBackdropClosing: false
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -92,7 +87,6 @@ export const Dialog = ({ context = "global" }) => {
|
||||
if (data.context !== context) return;
|
||||
setDialogInfo(data);
|
||||
setVisible(true);
|
||||
setInputValue(data.defaultValue);
|
||||
},
|
||||
[context]
|
||||
);
|
||||
@@ -143,9 +137,6 @@ export const Dialog = ({ context = "global" }) => {
|
||||
paragraph={dialogInfo.paragraph}
|
||||
paragraphColor={dialogInfo.paragraphColor}
|
||||
padding={12}
|
||||
style={{
|
||||
minHeight: 0
|
||||
}}
|
||||
/>
|
||||
<Seperator half />
|
||||
|
||||
@@ -172,28 +163,6 @@ export const Dialog = ({ context = "global" }) => {
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{dialogInfo.check ? (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setInputValue(!inputValue);
|
||||
}}
|
||||
icon={
|
||||
inputValue
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
style={{
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
height={35}
|
||||
width="100%"
|
||||
title={dialogInfo.check.info}
|
||||
type={inputValue ? dialogInfo.check.type : "gray"}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<DialogButtons
|
||||
onPressNegative={onNegativePress}
|
||||
onPressPositive={dialogInfo.positivePress && onPressPositive}
|
||||
|
||||
@@ -59,12 +59,14 @@ export class AddTopicDialog extends React.Component {
|
||||
|
||||
addNewTopic = async () => {
|
||||
try {
|
||||
this.setState({ loading: true });
|
||||
if (!this.title || this.title?.trim() === "") {
|
||||
ToastEvent.show({
|
||||
heading: "Topic title is required",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
this.setState({ loading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -76,11 +78,10 @@ export class AddTopicDialog extends React.Component {
|
||||
|
||||
await db.notebooks.notebook(topic.notebookId).topics.add(topic);
|
||||
}
|
||||
this.setState({ loading: false });
|
||||
this.close();
|
||||
setTimeout(() => {
|
||||
Navigation.queueRoutesForUpdate("Notebooks", "Notebook", "TopicNotes");
|
||||
useMenuStore.getState().setMenuPins();
|
||||
});
|
||||
Navigation.queueRoutesForUpdate("Notebooks", "Notebook", "TopicNotes");
|
||||
useMenuStore.getState().setMenuPins();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
@@ -176,6 +177,7 @@ export class AddTopicDialog extends React.Component {
|
||||
positiveTitle={this.toEdit ? "Save" : "Add"}
|
||||
onPressNegative={() => this.close()}
|
||||
onPressPositive={() => this.addNewTopic()}
|
||||
loading={this.state.loading}
|
||||
/>
|
||||
</DialogContainer>
|
||||
<Toast context="local" />
|
||||
|
||||
@@ -30,6 +30,7 @@ import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { eScrollEvent } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useCallback } from "react";
|
||||
import Tag from "../ui/tag";
|
||||
|
||||
@@ -112,15 +113,24 @@ export const Title = () => {
|
||||
<Heading
|
||||
onPress={navigateToNotebook}
|
||||
numberOfLines={isTopic ? 2 : 1}
|
||||
size={SIZE.xl}
|
||||
size={isTopic ? SIZE.md + 2 : SIZE.xl}
|
||||
style={{
|
||||
flexWrap: "wrap",
|
||||
marginTop: Platform.OS === "ios" ? -1 : 0
|
||||
}}
|
||||
color={currentScreen.color || colors.heading}
|
||||
>
|
||||
{isTopic ? (
|
||||
<Paragraph numberOfLines={1} size={SIZE.xs + 1}>
|
||||
{notebook?.title}
|
||||
{"\n"}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
{isTag ? (
|
||||
<Heading size={SIZE.xl} color={colors.accent}>
|
||||
<Heading
|
||||
size={isTopic ? SIZE.md + 2 : SIZE.xl}
|
||||
color={colors.accent}
|
||||
>
|
||||
#
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
@@ -54,6 +54,7 @@ import Config from "react-native-config";
|
||||
import { getGithubVersion } from "../../utils/github-version";
|
||||
import notifee from "@notifee/react-native";
|
||||
|
||||
|
||||
const Launcher = React.memo(
|
||||
function Launcher() {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -150,7 +151,7 @@ const Launcher = React.memo(
|
||||
}, [introCompleted]);
|
||||
|
||||
const checkAppUpdateAvailable = async () => {
|
||||
if (__DEV__ || Config.isTesting === "true") return;
|
||||
if (__DEV__) return;
|
||||
try {
|
||||
const version =
|
||||
Config.GITHUB_RELEASE === "true"
|
||||
|
||||
@@ -24,7 +24,6 @@ import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { db } from "../../../common/database";
|
||||
import Notebook from "../../../screens/notebook";
|
||||
import { TaggedNotes } from "../../../screens/notes/tagged";
|
||||
import { TopicNotes } from "../../../screens/notes/topic-notes";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
@@ -40,6 +39,10 @@ import { TimeSince } from "../../ui/time-since";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
const navigateToTopic = (topic) => {
|
||||
TopicNotes.navigate(topic, true);
|
||||
};
|
||||
|
||||
function navigateToTag(item) {
|
||||
const tag = db.tags.tag(item.id);
|
||||
if (!tag) return;
|
||||
@@ -52,29 +55,24 @@ const showActionSheet = (item) => {
|
||||
|
||||
function getNotebook(item) {
|
||||
const isTrash = item.type === "trash";
|
||||
if (isTrash) return [];
|
||||
const items = [];
|
||||
const notebooks = db.relations.to(item, "notebook") || [];
|
||||
if (isTrash || !item.notebooks || item.notebooks.length < 1) return [];
|
||||
|
||||
for (let notebook of notebooks) {
|
||||
if (items.length > 1) break;
|
||||
items.push(notebook);
|
||||
}
|
||||
return item.notebooks.reduce(function (prev, curr) {
|
||||
if (prev && prev.length > 0) return prev;
|
||||
const topicId = curr.topics[0];
|
||||
const notebook = db.notebooks?.notebook(curr.id)?.data;
|
||||
if (!notebook) return [];
|
||||
const topic = notebook.topics.find((t) => t.id === topicId);
|
||||
if (!topic) return [];
|
||||
|
||||
if (item.notebooks) {
|
||||
for (let nb of item.notebooks) {
|
||||
if (items.length > 1) break;
|
||||
const notebook = db.notebooks?.notebook(nb.id)?.data;
|
||||
if (!notebook) continue;
|
||||
for (let topicId of nb.topics) {
|
||||
if (items.length > 1) break;
|
||||
const topic = notebook.topics.find((t) => t.id === topicId);
|
||||
if (!topic) continue;
|
||||
items.push(topic);
|
||||
return [
|
||||
{
|
||||
title: `${notebook?.title} › ${topic?.title}`,
|
||||
notebook: notebook,
|
||||
topic: topic
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
];
|
||||
}, []);
|
||||
}
|
||||
|
||||
const NoteItem = ({
|
||||
@@ -90,11 +88,10 @@ const NoteItem = ({
|
||||
);
|
||||
const compactMode = notesListMode === "compact";
|
||||
const attachmentCount = db.attachments?.ofNote(item.id, "all")?.length || 0;
|
||||
const _update = useRelationStore((state) => state.updater);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const notebooks = React.useMemo(() => getNotebook(item), [item, _update]);
|
||||
const notebooks = React.useMemo(() => getNotebook(item), [item]);
|
||||
const reminders = db.relations.from(item, "reminder");
|
||||
const reminder = getUpcomingReminder(reminders);
|
||||
const _update = useRelationStore((state) => state.updater);
|
||||
const noteColor = COLORS_NOTE[item.color?.toLowerCase()];
|
||||
return (
|
||||
<>
|
||||
@@ -115,12 +112,12 @@ const NoteItem = ({
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
>
|
||||
{notebooks?.map((item) => (
|
||||
{notebooks?.map((_item) => (
|
||||
<Button
|
||||
title={item.title}
|
||||
key={item}
|
||||
title={_item.title}
|
||||
key={_item}
|
||||
height={25}
|
||||
icon={item.type === "topic" ? "bookmark" : "book-outline"}
|
||||
icon="book-outline"
|
||||
type="grayBg"
|
||||
fontSize={SIZE.xs}
|
||||
iconSize={SIZE.sm}
|
||||
@@ -135,13 +132,7 @@ const NoteItem = ({
|
||||
paddingHorizontal: 6,
|
||||
marginBottom: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
if (item.type === "topic") {
|
||||
TopicNotes.navigate(item, true);
|
||||
} else {
|
||||
Notebook.navigate(item);
|
||||
}
|
||||
}}
|
||||
onPress={() => navigateToTopic(_item.topic)}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ const RenderItem = ({ item, index, type, ...restArgs }) => {
|
||||
};
|
||||
})
|
||||
.filter((t) => t !== null) || [];
|
||||
|
||||
return (
|
||||
<Item
|
||||
item={item}
|
||||
@@ -104,9 +103,7 @@ const List = ({
|
||||
ListHeader,
|
||||
warning,
|
||||
isSheet = false,
|
||||
onMomentumScrollEnd,
|
||||
handlers,
|
||||
ScrollComponent
|
||||
onMomentumScrollEnd
|
||||
}) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const scrollRef = useRef();
|
||||
@@ -161,7 +158,7 @@ const List = ({
|
||||
};
|
||||
|
||||
const _keyExtractor = (item) => item.id || item.title;
|
||||
const ListView = ScrollComponent ? ScrollComponent : FlashList;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Animated.View
|
||||
@@ -170,8 +167,7 @@ const List = ({
|
||||
}}
|
||||
entering={type === "search" ? undefined : FadeInDown}
|
||||
>
|
||||
<ListView
|
||||
{...handlers}
|
||||
<FlashList
|
||||
style={styles}
|
||||
ref={scrollRef}
|
||||
testID={notesnook.list.id}
|
||||
|
||||
@@ -89,7 +89,6 @@ export const ColorTags = ({ item }) => {
|
||||
flexWrap: "wrap",
|
||||
flexGrow: isTablet ? undefined : 1,
|
||||
paddingHorizontal: 12,
|
||||
paddingRight: 0,
|
||||
alignItems: "center",
|
||||
justifyContent: isTablet ? "center" : "space-between"
|
||||
}}
|
||||
|
||||
@@ -16,9 +16,10 @@ 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 { Platform, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
@@ -36,6 +37,7 @@ import { Items } from "./items";
|
||||
import Notebooks from "./notebooks";
|
||||
import { Synced } from "./synced";
|
||||
import { Tags, TagStrip } from "./tags";
|
||||
|
||||
const Line = ({ top = 6, bottom = 6 }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
return (
|
||||
@@ -51,11 +53,20 @@ const Line = ({ top = 6, bottom = 6 }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
export const Properties = ({
|
||||
close = () => {},
|
||||
item,
|
||||
buttons = [],
|
||||
getRef
|
||||
}) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const alias = item.alias || item.title;
|
||||
const isColor = !!COLORS_NOTE[item.title];
|
||||
|
||||
const onScrollEnd = () => {
|
||||
getRef().current?.handleChildScrollEnd();
|
||||
};
|
||||
|
||||
if (!item || !item.id) {
|
||||
return (
|
||||
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
|
||||
@@ -64,92 +75,89 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FlatList
|
||||
<ScrollView
|
||||
nestedScrollEnabled
|
||||
onMomentumScrollEnd={onScrollEnd}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="none"
|
||||
style={{
|
||||
backgroundColor: colors.bg,
|
||||
paddingHorizontal: 0,
|
||||
borderBottomRightRadius: DDS.isLargeTablet() ? 10 : 1,
|
||||
borderBottomLeftRadius: DDS.isLargeTablet() ? 10 : 1,
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
data={[0]}
|
||||
keyExtractor={() => "properties-scroll-item"}
|
||||
renderItem={() => (
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginTop: 5,
|
||||
zIndex: 10
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>
|
||||
{item.type === "tag" && !isColor ? (
|
||||
<Heading size={SIZE.xl} color={colors.accent}>
|
||||
#
|
||||
</Heading>
|
||||
) : null}
|
||||
{alias}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginTop: 5,
|
||||
zIndex: 10
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>
|
||||
{item.type === "tag" && !isColor ? (
|
||||
<Heading size={SIZE.xl} color={colors.accent}>
|
||||
#
|
||||
</Heading>
|
||||
|
||||
{item.type === "note" ? (
|
||||
<TagStrip close={close} item={item} />
|
||||
) : null}
|
||||
|
||||
{item.type === "reminder" ? (
|
||||
<ReminderTime
|
||||
reminder={item}
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
borderWidth: 0,
|
||||
height: 30,
|
||||
alignSelf: "flex-start",
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
fontSize={SIZE.xs + 1}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
<Line top={12} />
|
||||
|
||||
<DateMeta item={item} />
|
||||
<Line bottom={0} />
|
||||
{item.type === "note" ? <Tags close={close} item={item} /> : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Notebooks note={item} close={close} />
|
||||
</View>
|
||||
|
||||
<Items
|
||||
item={item}
|
||||
buttons={buttons}
|
||||
close={() => {
|
||||
close();
|
||||
setTimeout(() => {
|
||||
SearchService.updateAndSearch();
|
||||
}, 1000);
|
||||
}}
|
||||
/>
|
||||
<Synced item={item} close={close} />
|
||||
<DevMode item={item} />
|
||||
|
||||
{DDS.isTab ? (
|
||||
<View
|
||||
style={{
|
||||
height: 20
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<SheetProvider context="properties" />
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
{alias}
|
||||
</Heading>
|
||||
|
||||
{item.type === "note" ? <TagStrip close={close} item={item} /> : null}
|
||||
|
||||
{item.type === "reminder" ? (
|
||||
<ReminderTime
|
||||
reminder={item}
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
borderWidth: 0,
|
||||
height: 30,
|
||||
alignSelf: "flex-start",
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
fontSize={SIZE.xs + 1}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
<Line top={12} />
|
||||
|
||||
<DateMeta item={item} />
|
||||
<Line bottom={0} />
|
||||
{item.type === "note" ? <Tags close={close} item={item} /> : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{item.notebooks ? <Notebooks note={item} close={close} /> : null}
|
||||
</View>
|
||||
|
||||
<Items
|
||||
item={item}
|
||||
buttons={buttons}
|
||||
close={() => {
|
||||
close();
|
||||
setTimeout(() => {
|
||||
SearchService.updateAndSearch();
|
||||
}, 1000);
|
||||
}}
|
||||
/>
|
||||
<Synced item={item} close={close} />
|
||||
<DevMode item={item} />
|
||||
|
||||
{DDS.isTab ? (
|
||||
<View
|
||||
style={{
|
||||
height: 20
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<SheetProvider context="properties" />
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -179,7 +187,6 @@ Properties.present = (item, buttons = [], isSheet) => {
|
||||
"lock-unlock",
|
||||
"trash",
|
||||
"remove-from-topic",
|
||||
"remove-from-notebook",
|
||||
"history",
|
||||
"read-only",
|
||||
"reminders",
|
||||
@@ -212,13 +219,12 @@ Properties.present = (item, buttons = [], isSheet) => {
|
||||
if (!props[0]) return;
|
||||
presentSheet({
|
||||
context: isSheet ? "local" : undefined,
|
||||
enableGesturesInScrollView: true,
|
||||
component: (ref, close) => (
|
||||
<Properties
|
||||
close={() => {
|
||||
close();
|
||||
}}
|
||||
actionSheetRef={ref}
|
||||
getRef={() => ref}
|
||||
item={props[0]}
|
||||
buttons={props[1]}
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { FlatList, ScrollView, View } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useActions } from "../../hooks/use-actions";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
@@ -84,9 +85,8 @@ export const Items = ({ item, buttons, close }) => {
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderColumnItem = (item) => (
|
||||
const renderColumnItem = ({ item }) => (
|
||||
<Button
|
||||
key={item.name + item.title}
|
||||
buttonType={{
|
||||
text: item.on
|
||||
? colors.accent
|
||||
@@ -108,8 +108,7 @@ export const Items = ({ item, buttons, close }) => {
|
||||
/>
|
||||
);
|
||||
|
||||
const renderTopBarItem = (item, index) => {
|
||||
const isLast = index === topBarItems.length;
|
||||
const renderTopBarItem = ({ item }) => {
|
||||
return (
|
||||
<PressableButton
|
||||
onPress={item.func}
|
||||
@@ -119,7 +118,7 @@ export const Items = ({ item, buttons, close }) => {
|
||||
alignItems: "center",
|
||||
width: topBarItemWidth,
|
||||
marginBottom: 10,
|
||||
marginRight: isLast ? 0 : 10,
|
||||
marginRight: 10,
|
||||
backgroundColor: "transparent"
|
||||
}}
|
||||
>
|
||||
@@ -170,7 +169,7 @@ export const Items = ({ item, buttons, close }) => {
|
||||
"lock-unlock",
|
||||
"publish"
|
||||
];
|
||||
const topBarItems = data.filter(
|
||||
const bottomBarItems = data.filter(
|
||||
(item) => topBarItemsList.indexOf(item.id) > -1
|
||||
);
|
||||
|
||||
@@ -179,19 +178,21 @@ export const Items = ({ item, buttons, close }) => {
|
||||
);
|
||||
|
||||
const topBarItemWidth =
|
||||
(width - (topBarItems.length * 10 + 14)) / topBarItems.length;
|
||||
(width - (bottomBarItems.length * 10 + 14)) / bottomBarItems.length;
|
||||
|
||||
return item.type === "note" ? (
|
||||
<>
|
||||
<ScrollView
|
||||
<FlatList
|
||||
data={bottomBarItems}
|
||||
keyExtractor={(item) => item.title}
|
||||
horizontal
|
||||
disableVirtualization={true}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 12
|
||||
}}
|
||||
>
|
||||
{topBarItems.map(renderTopBarItem)}
|
||||
</ScrollView>
|
||||
renderItem={renderTopBarItem}
|
||||
/>
|
||||
|
||||
<FlatList
|
||||
data={bottomGridItems}
|
||||
@@ -201,8 +202,10 @@ export const Items = ({ item, buttons, close }) => {
|
||||
disableVirtualization={true}
|
||||
style={{
|
||||
marginTop: item.type !== "note" ? 10 : 0,
|
||||
paddingTop: 10,
|
||||
marginLeft: 6
|
||||
paddingTop: 10
|
||||
}}
|
||||
columnWrapperStyle={{
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
alignSelf: "center",
|
||||
@@ -213,6 +216,11 @@ export const Items = ({ item, buttons, close }) => {
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<View data={data}>{data.map(renderColumnItem)}</View>
|
||||
<FlatList
|
||||
data={data}
|
||||
keyExtractor={(item) => item.title}
|
||||
renderItem={renderColumnItem}
|
||||
disableVirtualization={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,16 +35,8 @@ export default function Notebooks({ note, close, full }) {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const notebooks = useNotebookStore((state) => state.notebooks);
|
||||
function getNotebooks(item) {
|
||||
if (!item.notebooks || item.notebooks.length < 1) return [];
|
||||
let filteredNotebooks = [];
|
||||
const relations = db.relations.to(note, "notebook");
|
||||
filteredNotebooks.push(
|
||||
...relations.map((notebook) => ({
|
||||
...notebook,
|
||||
topics: []
|
||||
}))
|
||||
);
|
||||
if (!item.notebooks || item.notebooks.length < 1) return filteredNotebooks;
|
||||
|
||||
for (let notebookReference of item.notebooks) {
|
||||
let notebook = {
|
||||
...(notebooks.find((item) => item.id === notebookReference.id) || {})
|
||||
@@ -53,14 +45,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
notebook.topics = notebook.topics.filter((topic) => {
|
||||
return notebookReference.topics.findIndex((t) => t === topic.id) > -1;
|
||||
});
|
||||
const index = filteredNotebooks.findIndex(
|
||||
(item) => item.id === notebook.id
|
||||
);
|
||||
if (index > -1) {
|
||||
filteredNotebooks[index].topics = notebook.topics;
|
||||
} else {
|
||||
filteredNotebooks.push(notebook);
|
||||
}
|
||||
filteredNotebooks.push(notebook);
|
||||
}
|
||||
}
|
||||
return filteredNotebooks;
|
||||
@@ -78,6 +63,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
if (!item) return;
|
||||
TopicNotes.navigate(item, true);
|
||||
};
|
||||
|
||||
const renderItem = (item) => (
|
||||
<View
|
||||
key={item.id}
|
||||
@@ -92,8 +78,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
borderWidth: full ? 0 : 1,
|
||||
borderColor: colors.nav,
|
||||
borderRadius: 10,
|
||||
backgroundColor: full ? "transparent" : colors.nav,
|
||||
minHeight: 42
|
||||
backgroundColor: full ? "transparent" : colors.nav
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
@@ -175,7 +160,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
</View>
|
||||
);
|
||||
|
||||
return noteNotebooks.length === 0 ? null : (
|
||||
return !note.notebooks || note.notebooks.length === 0 ? null : (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
|
||||
@@ -21,10 +21,11 @@ import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { TaggedNotes } from "../../screens/notes/tagged";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { eOpenTagsDialog } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import ManageTagsSheet from "../sheets/manage-tags";
|
||||
import { Button } from "../ui/button";
|
||||
import { ColorTags } from "./color-tags";
|
||||
export const Tags = ({ item, close }) => {
|
||||
@@ -40,14 +41,14 @@ export const Tags = ({ item, close }) => {
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
alignSelf: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%"
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onPress={async () => {
|
||||
ManageTagsSheet.present(item);
|
||||
close();
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenTagsDialog, item);
|
||||
}}
|
||||
buttonType={{
|
||||
text: colors.accent
|
||||
@@ -78,8 +79,8 @@ export const TagStrip = ({ item, close }) => {
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{item.tags.map((tag) =>
|
||||
tag ? <TagItem key={tag} tag={tag} close={close} /> : null
|
||||
{item.tags.map((item) =>
|
||||
item ? <TagItem key={item} tag={item} close={close} /> : null
|
||||
)}
|
||||
</View>
|
||||
) : null;
|
||||
@@ -101,6 +102,7 @@ const TagItem = ({ tag, close }) => {
|
||||
marginTop: 0,
|
||||
backgroundColor: "transparent"
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
onPress={onPress}
|
||||
|
||||
@@ -201,7 +201,7 @@ export const SelectionHeader = React.memo(() => {
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md} color={colors.accent}>
|
||||
{selectedItemsList.length}
|
||||
{selectedItemsList.length + " Selected"}
|
||||
</Heading>
|
||||
</View>
|
||||
</View>
|
||||
@@ -264,29 +264,19 @@ export const SelectionHeader = React.memo(() => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{screen === "TopicNotes" || screen === "Notebook" ? (
|
||||
{screen === "TopicNotes" ? (
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
if (selectedItemsList.length > 0) {
|
||||
const currentScreen =
|
||||
const currentTopic =
|
||||
useNavigationStore.getState().currentScreen;
|
||||
|
||||
if (screen === "Notebook") {
|
||||
for (const item of selectedItemsList) {
|
||||
await db.relations.unlink(
|
||||
{ type: "notebook", id: currentScreen.id },
|
||||
item
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await db.notes.removeFromNotebook(
|
||||
{
|
||||
id: currentScreen.notebookId,
|
||||
topic: currentScreen.id
|
||||
},
|
||||
...selectedItemsList.map((item) => item.id)
|
||||
);
|
||||
}
|
||||
await db.notes.removeFromNotebook(
|
||||
{
|
||||
id: currentTopic.notebookId,
|
||||
topic: currentTopic.id
|
||||
},
|
||||
...selectedItemsList.map((item) => item.id)
|
||||
);
|
||||
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
@@ -303,9 +293,7 @@ export const SelectionHeader = React.memo(() => {
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
tooltipText={`Remove from ${
|
||||
screen === "Notebook" ? "notebook" : "topic"
|
||||
}`}
|
||||
tooltipText="Remove from topic"
|
||||
tooltipPosition={4}
|
||||
testID="select-minus"
|
||||
color={colors.pri}
|
||||
|
||||
@@ -36,7 +36,7 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
const SheetProvider = ({ context = "global" }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [data, setData] = useState(null);
|
||||
const [dialogData, setDialogData] = useState(null);
|
||||
const actionSheetRef = useRef();
|
||||
const editor = useRef({
|
||||
refocus: false
|
||||
@@ -52,35 +52,42 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
}, [close, open, visible]);
|
||||
|
||||
const open = useCallback(
|
||||
async (payload) => {
|
||||
if (!payload.context) payload.context = "global";
|
||||
if (payload.context !== context) return;
|
||||
setData(payload);
|
||||
async (data) => {
|
||||
if (!data.context) data.context = "global";
|
||||
if (data.context !== context) return;
|
||||
if (visible || dialogData) {
|
||||
setDialogData(null);
|
||||
setVisible(false);
|
||||
await sleep(0);
|
||||
}
|
||||
setDialogData(data);
|
||||
setVisible(true);
|
||||
if (payload.editor) {
|
||||
if (data.editor) {
|
||||
editor.current.refocus = false;
|
||||
if (editorState().keyboardState) {
|
||||
// tiny.call(EditorWebView, tiny.cacheRange + tiny.blur);
|
||||
editor.current.refocus = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
[context]
|
||||
[context, dialogData, visible]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (visible && data) {
|
||||
if (data.editor) await sleep(100);
|
||||
if (visible && dialogData) {
|
||||
if (dialogData.editor) await sleep(100);
|
||||
actionSheetRef.current?.setModalVisible(true);
|
||||
return;
|
||||
} else {
|
||||
if (editor.current?.refocus) {
|
||||
editorState().isFocused = true;
|
||||
// tiny.call(EditorWebView, tiny.restoreRange + tiny.clearRange);
|
||||
editor.current.refocus = false;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [visible, data]);
|
||||
}, [visible, dialogData]);
|
||||
|
||||
const close = useCallback(
|
||||
(ctx) => {
|
||||
@@ -91,30 +98,34 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
[context]
|
||||
);
|
||||
|
||||
return !visible || !data ? null : (
|
||||
return !visible || !dialogData ? null : (
|
||||
<SheetWrapper
|
||||
fwdRef={actionSheetRef}
|
||||
gestureEnabled={!data?.progress && !data?.disableClosing}
|
||||
closeOnTouchBackdrop={!data?.progress && !data?.disableClosing}
|
||||
gestureEnabled={!dialogData?.progress && !dialogData?.disableClosing}
|
||||
closeOnTouchBackdrop={
|
||||
!dialogData?.progress && !dialogData?.disableClosing
|
||||
}
|
||||
onClose={() => {
|
||||
data.onClose && data.onClose();
|
||||
dialogData.onClose && dialogData.onClose();
|
||||
setVisible(false);
|
||||
setData(null);
|
||||
setDialogData(null);
|
||||
}}
|
||||
enableGesturesInScrollView={data.enableGesturesInScrollView}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginBottom:
|
||||
!data.progress && !data.icon && !data.title && !data.paragraph
|
||||
!dialogData.progress &&
|
||||
!dialogData.icon &&
|
||||
!dialogData.title &&
|
||||
!dialogData.paragraph
|
||||
? 0
|
||||
: 10,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{data?.progress ? (
|
||||
{dialogData?.progress ? (
|
||||
<ActivityIndicator
|
||||
style={{
|
||||
marginTop: 15
|
||||
@@ -124,48 +135,47 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{data?.icon ? (
|
||||
{dialogData?.icon ? (
|
||||
<Icon
|
||||
color={colors[data.iconColor] || colors.accent}
|
||||
name={data.icon}
|
||||
color={colors[dialogData.iconColor] || colors.accent}
|
||||
name={dialogData.icon}
|
||||
size={50}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{data?.title ? <Heading> {data?.title}</Heading> : null}
|
||||
{dialogData?.title ? <Heading> {dialogData?.title}</Heading> : null}
|
||||
|
||||
{data?.paragraph ? (
|
||||
{dialogData?.paragraph ? (
|
||||
<Paragraph style={{ textAlign: "center" }}>
|
||||
{data?.paragraph}
|
||||
{dialogData?.paragraph}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{typeof data.component === "function"
|
||||
? data.component(
|
||||
{typeof dialogData.component === "function"
|
||||
? dialogData.component(
|
||||
actionSheetRef,
|
||||
() => close(context),
|
||||
(data) => {
|
||||
if (!data) return;
|
||||
setData((prevData) => {
|
||||
setDialogData((prevData) => {
|
||||
return {
|
||||
...prevData,
|
||||
...data
|
||||
};
|
||||
});
|
||||
},
|
||||
colors
|
||||
}
|
||||
)
|
||||
: data.component}
|
||||
: dialogData.component}
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: data.valueArray ? 12 : 0
|
||||
marginBottom: dialogData.valueArray ? 12 : 0
|
||||
}}
|
||||
>
|
||||
{data.valueArray &&
|
||||
data.valueArray.map((v) => (
|
||||
{dialogData.valueArray &&
|
||||
dialogData.valueArray.map((v) => (
|
||||
<Button
|
||||
title={v}
|
||||
type="gray"
|
||||
@@ -187,12 +197,12 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{data?.action ? (
|
||||
{dialogData?.action ? (
|
||||
<Button
|
||||
onPress={data.action}
|
||||
key={data.actionText}
|
||||
title={data.actionText}
|
||||
accentColor={data.iconColor || "accent"}
|
||||
onPress={dialogData.action}
|
||||
key={dialogData.actionText}
|
||||
title={dialogData.actionText}
|
||||
accentColor={dialogData.iconColor || "accent"}
|
||||
accentText="light"
|
||||
type="accent"
|
||||
height={45}
|
||||
@@ -204,8 +214,8 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{data?.actionsArray &&
|
||||
data?.actionsArray.map((item) => (
|
||||
{dialogData?.actionsArray &&
|
||||
dialogData?.actionsArray.map((item) => (
|
||||
<Button
|
||||
onPress={item.action}
|
||||
key={item.accentText}
|
||||
@@ -221,7 +231,7 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
/>
|
||||
))}
|
||||
|
||||
{data?.learnMore ? (
|
||||
{dialogData?.learnMore ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
@@ -229,7 +239,7 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
onPress={data.learnMorePress}
|
||||
onPress={dialogData.learnMorePress}
|
||||
color={colors.icon}
|
||||
>
|
||||
<Icon
|
||||
@@ -237,7 +247,7 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
name="information-outline"
|
||||
size={SIZE.xs}
|
||||
/>{" "}
|
||||
{data.learnMore}
|
||||
{dialogData.learnMore}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -25,34 +25,41 @@ import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { db } from "../../../common/database";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import { presentSheet, ToastEvent } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useMenuStore } from "../../../stores/use-menu-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
eCloseAddNotebookDialog,
|
||||
eOpenAddNotebookDialog
|
||||
} from "../../../utils/events";
|
||||
import { ph, pv, SIZE } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { Button } from "../../ui/button";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import { MoveNotes } from "../move-notes/movenote";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import { Toast } from "../../toast";
|
||||
|
||||
let refs = [];
|
||||
|
||||
export class AddNotebookSheet extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
refs = [];
|
||||
this.state = {
|
||||
notebook: props.notebook,
|
||||
topics:
|
||||
props.notebook?.topics?.map((item) => {
|
||||
return item.title;
|
||||
}) || [],
|
||||
notebook: null,
|
||||
visible: false,
|
||||
topics: [],
|
||||
description: null,
|
||||
titleFocused: false,
|
||||
descFocused: false,
|
||||
@@ -61,14 +68,13 @@ export class AddNotebookSheet extends React.Component {
|
||||
editTopic: false,
|
||||
loading: false
|
||||
};
|
||||
|
||||
this.title = props.notebook?.title;
|
||||
this.description = props.notebook?.description;
|
||||
this.title = null;
|
||||
this.description = null;
|
||||
this.listRef;
|
||||
this.prevItem = null;
|
||||
this.prevIndex = null;
|
||||
this.currentSelectedInput = null;
|
||||
this.id = props.notebook?.id;
|
||||
this.id = null;
|
||||
this.backPressCount = 0;
|
||||
this.currentInputValue = null;
|
||||
this.titleRef;
|
||||
@@ -77,22 +83,57 @@ export class AddNotebookSheet extends React.Component {
|
||||
this.hiddenInput = createRef();
|
||||
this.topicInputRef = createRef();
|
||||
this.addingTopic = false;
|
||||
this.actionSheetRef = props.actionSheetRef;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
refs = [];
|
||||
this.actionSheetRef = createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
sleep(300).then(() => {
|
||||
!this.state.notebook && this.titleRef?.focus();
|
||||
});
|
||||
eSubscribeEvent(eOpenAddNotebookDialog, this.open);
|
||||
eSubscribeEvent(eCloseAddNotebookDialog, this.close);
|
||||
}
|
||||
|
||||
close = () => {
|
||||
componentWillUnmount() {
|
||||
eUnSubscribeEvent(eOpenAddNotebookDialog, this.open);
|
||||
eUnSubscribeEvent(eCloseAddNotebookDialog, this.close);
|
||||
}
|
||||
|
||||
open = (notebook) => {
|
||||
refs = [];
|
||||
this.props.close();
|
||||
|
||||
if (notebook) {
|
||||
let topicsList = [];
|
||||
notebook.topics.forEach((item) => {
|
||||
topicsList.push(item.title);
|
||||
});
|
||||
this.id = notebook.id;
|
||||
this.title = notebook.title;
|
||||
this.description = notebook.description;
|
||||
|
||||
this.setState({
|
||||
topics: [...topicsList],
|
||||
visible: true,
|
||||
notebook: notebook
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
visible: true,
|
||||
notebook: null
|
||||
});
|
||||
}
|
||||
sleep(100).then(() => {
|
||||
this.actionSheetRef.current?.show();
|
||||
});
|
||||
};
|
||||
|
||||
close = () => {
|
||||
this.actionSheetRef.current?.hide();
|
||||
refs = [];
|
||||
this.prevIndex = null;
|
||||
this.prevItem = null;
|
||||
this.currentSelectedInput = null;
|
||||
this.title = null;
|
||||
this.description = null;
|
||||
this.currentInputValue = null;
|
||||
this.id = null;
|
||||
};
|
||||
|
||||
onDelete = (index) => {
|
||||
@@ -199,8 +240,15 @@ export class AddNotebookSheet extends React.Component {
|
||||
"Notebooks",
|
||||
"Notebook"
|
||||
);
|
||||
useRelationStore.getState().update();
|
||||
MoveNotes.present(db.notebooks.notebook(newNotebookId).data);
|
||||
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
this.close();
|
||||
await sleep(300);
|
||||
if (!notebook) {
|
||||
MoveNotes.present(db.notebooks.notebook(newNotebookId).data);
|
||||
}
|
||||
};
|
||||
|
||||
onSubmit = (forward = true) => {
|
||||
@@ -217,7 +265,7 @@ export class AddNotebookSheet extends React.Component {
|
||||
topics: prevTopics
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.listRef.current?.scrollToEnd?.({ animated: true });
|
||||
this.listRef.scrollToEnd({ animated: true });
|
||||
}, 30);
|
||||
this.currentInputValue = null;
|
||||
} else {
|
||||
@@ -241,7 +289,7 @@ export class AddNotebookSheet extends React.Component {
|
||||
|
||||
if (forward) {
|
||||
setTimeout(() => {
|
||||
this.listRef.current?.scrollToEnd?.({ animated: true });
|
||||
this.listRef.scrollToEnd({ animated: true });
|
||||
}, 30);
|
||||
}
|
||||
}
|
||||
@@ -253,139 +301,166 @@ export class AddNotebookSheet extends React.Component {
|
||||
|
||||
render() {
|
||||
const { colors } = this.props;
|
||||
const { topics, topicInputFocused, notebook } = this.state;
|
||||
const { topics, visible, topicInputFocused, notebook } = this.state;
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
maxHeight: DDS.isTab ? "90%" : "96%",
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
paddingHorizontal: 12
|
||||
<SheetWrapper
|
||||
onOpen={async () => {
|
||||
this.topicsToDelete = [];
|
||||
await sleep(300);
|
||||
!this.state.notebook && this.titleRef?.focus();
|
||||
}}
|
||||
fwdRef={this.actionSheetRef}
|
||||
onClose={() => {
|
||||
this.close();
|
||||
this.setState({
|
||||
visible: false,
|
||||
topics: [],
|
||||
descFocused: false,
|
||||
titleFocused: false,
|
||||
editTopic: false,
|
||||
notebook: null
|
||||
});
|
||||
}}
|
||||
statusBarTranslucent={false}
|
||||
onRequestClose={this.close}
|
||||
>
|
||||
<TextInput
|
||||
ref={this.hiddenInput}
|
||||
<View
|
||||
style={{
|
||||
width: 1,
|
||||
height: 1,
|
||||
opacity: 0,
|
||||
position: "absolute"
|
||||
maxHeight: DDS.isTab ? "90%" : "96%",
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
<DialogHeader
|
||||
title={
|
||||
notebook && notebook.dateCreated ? "Edit Notebook" : "New Notebook"
|
||||
}
|
||||
paragraph={
|
||||
notebook && notebook.dateCreated
|
||||
? "You are editing " + this.title + " notebook."
|
||||
: "Notebooks are the best way to organize your notes."
|
||||
}
|
||||
/>
|
||||
<Seperator half />
|
||||
|
||||
<Input
|
||||
fwdRef={(ref) => (this.titleRef = ref)}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.title}
|
||||
onChangeText={(value) => {
|
||||
this.title = value;
|
||||
}}
|
||||
placeholder="Enter a title"
|
||||
onSubmit={() => {
|
||||
this.descriptionRef.focus();
|
||||
}}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.title : null}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={(ref) => (this.descriptionRef = ref)}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.description}
|
||||
onChangeText={(value) => {
|
||||
this.description = value;
|
||||
}}
|
||||
placeholder="Describe your notebook."
|
||||
onSubmit={() => {
|
||||
this.topicInputRef.current?.focus();
|
||||
}}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.description : null}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={this.topicInputRef}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.topic}
|
||||
onChangeText={(value) => {
|
||||
this.currentInputValue = value;
|
||||
if (this.prevItem !== null) {
|
||||
refs[this.prevIndex].setNativeProps({
|
||||
text: value,
|
||||
style: {
|
||||
borderBottomColor: colors.accent
|
||||
}
|
||||
});
|
||||
>
|
||||
<TextInput
|
||||
ref={this.hiddenInput}
|
||||
style={{
|
||||
width: 1,
|
||||
height: 1,
|
||||
opacity: 0,
|
||||
position: "absolute"
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
<DialogHeader
|
||||
title={
|
||||
notebook && notebook.dateCreated
|
||||
? "Edit Notebook"
|
||||
: "New Notebook"
|
||||
}
|
||||
}}
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
onSubmit={() => {
|
||||
this.onSubmit();
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
button={{
|
||||
testID: "topic-add-button",
|
||||
icon: this.state.editTopic ? "check" : "plus",
|
||||
onPress: this.onSubmit,
|
||||
color: topicInputFocused ? colors.accent : colors.icon
|
||||
}}
|
||||
placeholder="Add a topic"
|
||||
/>
|
||||
paragraph={
|
||||
notebook && notebook.dateCreated
|
||||
? "You are editing " + this.title + " notebook."
|
||||
: "Notebooks are the best way to organize your notes."
|
||||
}
|
||||
/>
|
||||
<Seperator half />
|
||||
|
||||
<FlatList
|
||||
data={topics}
|
||||
ref={(ref) => (this.listRef = ref)}
|
||||
nestedScrollEnabled
|
||||
keyExtractor={(item, index) => item + index.toString()}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="interactive"
|
||||
ListFooterComponent={<View style={{ height: 50 }} />}
|
||||
renderItem={({ item, index }) => (
|
||||
<TopicItem
|
||||
item={item}
|
||||
onPress={(item, index) => {
|
||||
this.prevIndex = index;
|
||||
this.prevItem = item;
|
||||
this.topicInputRef.current?.setNativeProps({
|
||||
text: item
|
||||
<Input
|
||||
fwdRef={(ref) => (this.titleRef = ref)}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.title}
|
||||
onChangeText={(value) => {
|
||||
this.title = value;
|
||||
}}
|
||||
placeholder="Enter a title"
|
||||
onSubmit={() => {
|
||||
this.descriptionRef.focus();
|
||||
}}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.title : null}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={(ref) => (this.descriptionRef = ref)}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.description}
|
||||
onChangeText={(value) => {
|
||||
this.description = value;
|
||||
}}
|
||||
placeholder="Describe your notebook."
|
||||
onSubmit={() => {
|
||||
this.topicInputRef.current?.focus();
|
||||
}}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.description : null}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={this.topicInputRef}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.topic}
|
||||
onChangeText={(value) => {
|
||||
this.currentInputValue = value;
|
||||
if (this.prevItem !== null) {
|
||||
refs[this.prevIndex].setNativeProps({
|
||||
text: value,
|
||||
style: {
|
||||
borderBottomColor: colors.accent
|
||||
}
|
||||
});
|
||||
this.topicInputRef.current?.focus();
|
||||
this.currentInputValue = item;
|
||||
this.setState({
|
||||
editTopic: true
|
||||
});
|
||||
}}
|
||||
onDelete={this.onDelete}
|
||||
index={index}
|
||||
colors={colors}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Seperator />
|
||||
<Button
|
||||
width="100%"
|
||||
height={50}
|
||||
fontSize={SIZE.md}
|
||||
title={
|
||||
notebook && notebook.dateCreated
|
||||
? "Save changes"
|
||||
: "Create notebook"
|
||||
}
|
||||
type="accent"
|
||||
onPress={this.addNewNotebook}
|
||||
/>
|
||||
{/*
|
||||
}
|
||||
}}
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
onSubmit={() => {
|
||||
this.onSubmit();
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
button={{
|
||||
testID: "topic-add-button",
|
||||
icon: this.state.editTopic ? "check" : "plus",
|
||||
onPress: this.onSubmit,
|
||||
color: topicInputFocused ? colors.accent : colors.icon
|
||||
}}
|
||||
placeholder="Add a topic"
|
||||
/>
|
||||
|
||||
<FlatList
|
||||
data={topics}
|
||||
ref={(ref) => (this.listRef = ref)}
|
||||
nestedScrollEnabled
|
||||
keyExtractor={(item, index) => item + index.toString()}
|
||||
onMomentumScrollEnd={() => {
|
||||
this.actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="interactive"
|
||||
ListFooterComponent={<View style={{ height: 50 }} />}
|
||||
renderItem={({ item, index }) => (
|
||||
<TopicItem
|
||||
item={item}
|
||||
onPress={(item, index) => {
|
||||
this.prevIndex = index;
|
||||
this.prevItem = item;
|
||||
this.topicInputRef.current?.setNativeProps({
|
||||
text: item
|
||||
});
|
||||
this.topicInputRef.current?.focus();
|
||||
this.currentInputValue = item;
|
||||
this.setState({
|
||||
editTopic: true
|
||||
});
|
||||
}}
|
||||
onDelete={this.onDelete}
|
||||
index={index}
|
||||
colors={colors}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Seperator />
|
||||
<Button
|
||||
width="100%"
|
||||
height={50}
|
||||
fontSize={SIZE.md}
|
||||
title={
|
||||
notebook && notebook.dateCreated
|
||||
? "Save changes"
|
||||
: "Create notebook"
|
||||
}
|
||||
type="accent"
|
||||
onPress={this.addNewNotebook}
|
||||
/>
|
||||
{/*
|
||||
{Platform.OS === 'ios' && (
|
||||
<View
|
||||
style={{
|
||||
@@ -393,24 +468,14 @@ export class AddNotebookSheet extends React.Component {
|
||||
}}
|
||||
/>
|
||||
)} */}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Toast context="local" />
|
||||
</SheetWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNotebookSheet.present = (notebook) => {
|
||||
presentSheet({
|
||||
component: (ref, close, _update, colors) => (
|
||||
<AddNotebookSheet
|
||||
actionSheetRef={ref}
|
||||
notebook={notebook}
|
||||
close={close}
|
||||
colors={colors}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
const TopicItem = ({ item, index, colors, onPress, onDelete }) => {
|
||||
const topicRef = (ref) => (refs[index] = ref);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import { db } from "../../../common/database";
|
||||
import { ListHeaderInputItem } from "./list-header-item.js";
|
||||
|
||||
@@ -27,7 +27,6 @@ export const FilteredList = ({
|
||||
itemType,
|
||||
onAddItem,
|
||||
hasHeaderSearch,
|
||||
listRef,
|
||||
...restProps
|
||||
}) => {
|
||||
const [filtered, setFiltered] = useState(data);
|
||||
@@ -57,7 +56,6 @@ export const FilteredList = ({
|
||||
<FlatList
|
||||
{...restProps}
|
||||
data={filtered}
|
||||
ref={listRef}
|
||||
ListHeaderComponent={
|
||||
hasHeaderSearch ? (
|
||||
<ListHeaderInputItem
|
||||
|
||||
@@ -17,37 +17,91 @@ 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, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import React, {
|
||||
createRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState
|
||||
} from "react";
|
||||
import { Keyboard, TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../../common/database";
|
||||
import { presentSheet, ToastEvent } from "../../../services/event-manager";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import SearchService from "../../../services/search";
|
||||
import { useNotebookStore } from "../../../stores/use-notebook-store";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { eOpenMoveNoteDialog } from "../../../utils/events";
|
||||
import { Dialog } from "../../dialog";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import { Button } from "../../ui/button";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { SelectionProvider } from "./context";
|
||||
import { FilteredList } from "./filtered-list";
|
||||
import { ListItem } from "./list-item";
|
||||
|
||||
const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
const actionSheetRef = createRef();
|
||||
const AddToNotebookSheet = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [note, setNote] = useState(null);
|
||||
|
||||
function open(note) {
|
||||
setNote(note);
|
||||
setVisible(true);
|
||||
actionSheetRef.current?.setModalVisible(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenMoveNoteDialog, open);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenMoveNoteDialog, open);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const _onClose = () => {
|
||||
setVisible(false);
|
||||
setNote(null);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"Notes",
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Notebooks",
|
||||
"Notebook"
|
||||
);
|
||||
};
|
||||
|
||||
return !visible ? null : (
|
||||
<SheetWrapper fwdRef={actionSheetRef} onClose={_onClose}>
|
||||
<MoveNoteComponent note={note} />
|
||||
</SheetWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddToNotebookSheet;
|
||||
|
||||
const MoveNoteComponent = ({ note }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [multiSelect, setMultiSelect] = useState(false);
|
||||
const notebooks = useNotebookStore((state) =>
|
||||
state.notebooks.filter((n) => n?.type === "notebook")
|
||||
);
|
||||
|
||||
const [edited, setEdited] = useState(false);
|
||||
const selectedItemsList = useSelectionStore(
|
||||
(state) => state.selectedItemsList
|
||||
);
|
||||
const setNotebooks = useNotebookStore((state) => state.setNotebooks);
|
||||
const [itemState, setItemState] = useState({});
|
||||
|
||||
const onAddNotebook = async (title) => {
|
||||
if (!title || title.trim().length === 0) {
|
||||
ToastEvent.show({
|
||||
@@ -105,13 +159,13 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
(item) => {
|
||||
switch (item.type) {
|
||||
case "notebook": {
|
||||
const notes = db.relations.from(item, "note");
|
||||
if (notes.length === 0) return 0;
|
||||
const noteIds = [];
|
||||
for (let topic of item.topics) {
|
||||
noteIds.push(...(db.notes?.topicReferences.get(topic.id) || []));
|
||||
}
|
||||
let count = 0;
|
||||
selectedItemsList.forEach((item) =>
|
||||
notes.findIndex((note) => note.id === item.id) > -1
|
||||
? count++
|
||||
: undefined
|
||||
noteIds.indexOf(item.id) > -1 ? count++ : undefined
|
||||
);
|
||||
return count;
|
||||
}
|
||||
@@ -141,7 +195,8 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
for (let notebook of notebooks) {
|
||||
itemState[notebook.id] = state
|
||||
? state
|
||||
: areAllSelectedItemsInNotebook(notebook, selectedItemsList)
|
||||
: areAllSelectedItemsInAllTopics(notebook, selectedItemsList) &&
|
||||
getSelectedNotesCountInItem(notebook, selectedItemsList) > 0
|
||||
? "selected"
|
||||
: getSelectedNotesCountInItem(notebook, selectedItemsList) > 0
|
||||
? "intermediate"
|
||||
@@ -176,11 +231,11 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
}
|
||||
};
|
||||
|
||||
function areAllSelectedItemsInNotebook(notebook, items) {
|
||||
const notes = db.relations.from(notebook, "note");
|
||||
if (notes.length === 0) return false;
|
||||
function areAllSelectedItemsInAllTopics(notebook, items) {
|
||||
return items.every((item) => {
|
||||
return notes.find((note) => note.id === item.id);
|
||||
return notebook.topics.every((topic) => {
|
||||
return db.notes.topicReferences.get(topic.id).indexOf(item.id) > -1;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -195,6 +250,25 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
const mergeState = {
|
||||
[item.id]: state
|
||||
};
|
||||
const notebooks = db.notebooks.all;
|
||||
const notebook =
|
||||
item.type === "notebook"
|
||||
? item
|
||||
: notebooks.find((n) => n.id === item.notebookId);
|
||||
const intermediate = notebook.topics.some((topic) => {
|
||||
return topic.id === item.id
|
||||
? state === "selected"
|
||||
: itemState[topic.id] === "selected";
|
||||
});
|
||||
if (intermediate) mergeState[notebook.id] = "intermediate";
|
||||
const selected = notebook.topics.every((topic) => {
|
||||
return topic.id === item.id
|
||||
? state === "selected"
|
||||
: itemState[topic.id] === "selected";
|
||||
});
|
||||
if (selected) mergeState[notebook.id] = "selected";
|
||||
if (!selected && !intermediate) mergeState[notebook.id] = "deselected";
|
||||
|
||||
return {
|
||||
...itemState,
|
||||
...mergeState
|
||||
@@ -240,39 +314,28 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
};
|
||||
|
||||
const onSave = async () => {
|
||||
const noteIds = note ? [note.id] : selectedItemsList.map((n) => n.id);
|
||||
for (const id in itemState) {
|
||||
const item = getItemFromId(id);
|
||||
if (item.type === "notebook") continue;
|
||||
const noteIds = selectedItemsList.map((n) => n.id);
|
||||
if (itemState[id] === "selected") {
|
||||
if (item.type === "notebook") {
|
||||
for (let noteId of noteIds) {
|
||||
db.relations.add(item, { id: noteId, type: "note" });
|
||||
}
|
||||
} else {
|
||||
await db.notes.addToNotebook(
|
||||
{
|
||||
topic: item.id,
|
||||
id: item.notebookId,
|
||||
rebuildCache: true
|
||||
},
|
||||
...noteIds
|
||||
);
|
||||
}
|
||||
await db.notes.addToNotebook(
|
||||
{
|
||||
topic: item.id,
|
||||
id: item.notebookId,
|
||||
rebuildCache: true
|
||||
},
|
||||
...noteIds
|
||||
);
|
||||
} else if (itemState[id] === "deselected") {
|
||||
if (item.type === "notebook") {
|
||||
for (let noteId of noteIds) {
|
||||
db.relations.unlink(item, { id: noteId, type: "note" });
|
||||
}
|
||||
} else {
|
||||
await db.notes.removeFromNotebook(
|
||||
{
|
||||
id: item.notebookId,
|
||||
topic: item.id,
|
||||
rebuildCache: true
|
||||
},
|
||||
...noteIds
|
||||
);
|
||||
}
|
||||
await db.notes.removeFromNotebook(
|
||||
{
|
||||
id: item.notebookId,
|
||||
topic: item.id,
|
||||
rebuildCache: true
|
||||
},
|
||||
...noteIds
|
||||
);
|
||||
}
|
||||
}
|
||||
Navigation.queueRoutesForUpdate(
|
||||
@@ -280,8 +343,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
"Favorites",
|
||||
"ColoredNotes",
|
||||
"TaggedNotes",
|
||||
"TopicNotes",
|
||||
"Notebook"
|
||||
"TopicNotes"
|
||||
);
|
||||
setNotebooks();
|
||||
SearchService.updateAndSearch();
|
||||
@@ -358,6 +420,9 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
|
||||
<SelectionProvider value={contextValue}>
|
||||
<FilteredList
|
||||
onMomentumScrollEnd={() => {
|
||||
actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
@@ -390,7 +455,6 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
itemState[item.id] === "deselected" &&
|
||||
getSelectedNotesCountInItem(item) > 0
|
||||
}
|
||||
sheetRef={actionSheetRef}
|
||||
isSelected={itemState[item.id] === "selected"}
|
||||
infoText={
|
||||
<>
|
||||
@@ -416,6 +480,10 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
if (currentState !== "selected") {
|
||||
resetItemState("deselected");
|
||||
contextValue.select(item);
|
||||
updateItemState(
|
||||
notebooks.find((n) => n.id === item.notebookId),
|
||||
"intermediate"
|
||||
);
|
||||
} else {
|
||||
contextValue.deselect(item);
|
||||
}
|
||||
@@ -446,24 +514,16 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
onAddItem={async (title) => {
|
||||
return await onAddNotebook(title);
|
||||
}}
|
||||
// ListFooterComponent={
|
||||
// <View
|
||||
// style={{
|
||||
// height: 200
|
||||
// }}
|
||||
// />
|
||||
// }
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 200
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SelectionProvider>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MoveNoteSheet.present = (note) => {
|
||||
presentSheet({
|
||||
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />,
|
||||
enableGesturesInScrollView: false
|
||||
});
|
||||
};
|
||||
export default MoveNoteSheet;
|
||||
|
||||
@@ -46,15 +46,25 @@ const _ListItem = ({
|
||||
removed,
|
||||
isSelected,
|
||||
hasHeaderSearch,
|
||||
onAddSublistItem,
|
||||
sheetRef
|
||||
onAddSublistItem
|
||||
}) => {
|
||||
const { enabled, toggleSelection, setMultiSelect } = useSelectionContext();
|
||||
const { enabled, toggleSelection, setMultiSelect, select, deselect } =
|
||||
useSelectionContext();
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
function selectItem() {
|
||||
const currentState = isSelected;
|
||||
toggleSelection(item);
|
||||
if (item.type === "notebook") {
|
||||
item.topics.forEach((item) => {
|
||||
if (currentState) {
|
||||
deselect(item);
|
||||
} else {
|
||||
select(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return (
|
||||
<View
|
||||
@@ -112,6 +122,9 @@ const _ListItem = ({
|
||||
: colors.icon
|
||||
}
|
||||
onPress={() => {
|
||||
if (item.type === "notebook") {
|
||||
setMultiSelect(true);
|
||||
}
|
||||
selectItem();
|
||||
if (enabled) return;
|
||||
onPress?.(item);
|
||||
@@ -192,7 +205,7 @@ const _ListItem = ({
|
||||
style={{
|
||||
width: "95%",
|
||||
alignSelf: "flex-end",
|
||||
maxHeight: 250
|
||||
maxHeight: 500
|
||||
}}
|
||||
itemType={sublistItemType}
|
||||
hasHeaderSearch={hasHeaderSearch}
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React, { RefObject, useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { ActionSheetRef } from "react-native-actions-sheet";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
eSendEvent,
|
||||
@@ -31,7 +31,7 @@ import { Button } from "../../ui/button";
|
||||
import Input from "../../ui/input";
|
||||
|
||||
type ChangeEmailProps = {
|
||||
actionSheetRef: RefObject<ActionSheetRef>;
|
||||
actionSheetRef: RefObject<ActionSheet>;
|
||||
close?: () => void;
|
||||
update?: (options: PresentSheetOptions) => void;
|
||||
};
|
||||
|
||||
@@ -17,32 +17,51 @@ 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, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../../common/database";
|
||||
import { presentSheet, ToastEvent } from "../../../services/event-manager";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useTagStore } from "../../../stores/use-tag-store";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { eCloseTagsDialog, eOpenTagsDialog } from "../../../utils/events";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import Input from "../../ui/input";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
const ManageTagsSheet = (props) => {
|
||||
import { useCallback } from "react";
|
||||
const ManageTagsSheet = () => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [note, setNote] = useState(props.note);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [note, setNote] = useState(null);
|
||||
const allTags = useTagStore((state) => state.tags);
|
||||
const [tags, setTags] = useState([]);
|
||||
const [query, setQuery] = useState(null);
|
||||
const inputRef = useRef();
|
||||
const [focus, setFocus] = useState(false);
|
||||
const actionSheetRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
sortTags();
|
||||
}, [allTags, note, query, sortTags]);
|
||||
eSubscribeEvent(eOpenTagsDialog, open);
|
||||
eSubscribeEvent(eCloseTagsDialog, close);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenTagsDialog, open);
|
||||
eUnSubscribeEvent(eCloseTagsDialog, close);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
sortTags();
|
||||
}
|
||||
}, [allTags, note, query, sortTags, visible]);
|
||||
|
||||
const sortTags = useCallback(() => {
|
||||
let _tags = [...allTags];
|
||||
@@ -69,9 +88,26 @@ const ManageTagsSheet = (props) => {
|
||||
setTags(combinedTags);
|
||||
}, [allTags, note, query]);
|
||||
|
||||
const open = useCallback(
|
||||
(item) => {
|
||||
setNote(item);
|
||||
useTagStore.getState().setTags();
|
||||
sortTags();
|
||||
setVisible(true);
|
||||
},
|
||||
[sortTags]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
useTagStore.getState().setTags();
|
||||
}, []);
|
||||
if (visible) {
|
||||
actionSheetRef.current?.show();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const close = () => {
|
||||
setQuery(null);
|
||||
actionSheetRef.current?.hide();
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
let _query = query;
|
||||
@@ -114,96 +150,104 @@ const ManageTagsSheet = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
minHeight: focus ? "100%" : "60%"
|
||||
return !visible ? null : (
|
||||
<SheetWrapper
|
||||
centered={false}
|
||||
fwdRef={actionSheetRef}
|
||||
onOpen={async () => {
|
||||
await sleep(300);
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
onClose={async () => {
|
||||
setQuery(null);
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
button={{
|
||||
icon: "magnify",
|
||||
color: colors.accent,
|
||||
size: SIZE.lg
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
minHeight: "60%"
|
||||
}}
|
||||
testID="tag-input"
|
||||
fwdRef={inputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(v) => {
|
||||
setQuery(db.tags.sanitize(v));
|
||||
}}
|
||||
onFocusInput={() => {
|
||||
setFocus(true);
|
||||
}}
|
||||
onBlurInput={() => {
|
||||
setFocus(false);
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
height={50}
|
||||
placeholder="Search or add a tag"
|
||||
/>
|
||||
|
||||
<ScrollView
|
||||
overScrollMode="never"
|
||||
scrollToOverflowEnabled={false}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
>
|
||||
{query && query !== tags[0]?.title ? (
|
||||
<PressableButton
|
||||
key={"query_item"}
|
||||
customStyle={{
|
||||
flexDirection: "row",
|
||||
marginVertical: 5,
|
||||
justifyContent: "space-between",
|
||||
padding: 12
|
||||
}}
|
||||
onPress={onSubmit}
|
||||
type="accent"
|
||||
>
|
||||
<Heading size={SIZE.sm} color={colors.light}>
|
||||
Add {'"' + "#" + query + '"'}
|
||||
</Heading>
|
||||
<Icon name="plus" color={colors.light} size={SIZE.lg} />
|
||||
</PressableButton>
|
||||
) : null}
|
||||
{!allTags || allTags.length === 0 ? (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 200,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={50} color={colors.icon}>
|
||||
#
|
||||
</Heading>
|
||||
<Paragraph textBreakStrategy="balanced" color={colors.icon}>
|
||||
You do not have any tags.
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
<Input
|
||||
button={{
|
||||
icon: "magnify",
|
||||
color: colors.accent,
|
||||
size: SIZE.lg
|
||||
}}
|
||||
testID="tag-input"
|
||||
fwdRef={inputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(v) => {
|
||||
setQuery(db.tags.sanitize(v));
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
height={50}
|
||||
placeholder="Search or add a tag"
|
||||
/>
|
||||
|
||||
{tags.map((item) => (
|
||||
<TagItem key={item.title} tag={item} note={note} setNote={setNote} />
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
<ScrollView
|
||||
nestedScrollEnabled
|
||||
overScrollMode="never"
|
||||
scrollToOverflowEnabled={false}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
onMomentumScrollEnd={() => {
|
||||
actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
>
|
||||
{query && query !== tags[0]?.title ? (
|
||||
<PressableButton
|
||||
key={"query_item"}
|
||||
customStyle={{
|
||||
flexDirection: "row",
|
||||
marginVertical: 5,
|
||||
justifyContent: "space-between",
|
||||
padding: 12
|
||||
}}
|
||||
onPress={onSubmit}
|
||||
type="accent"
|
||||
>
|
||||
<Heading size={SIZE.sm} color={colors.light}>
|
||||
Add {'"' + "#" + query + '"'}
|
||||
</Heading>
|
||||
<Icon name="plus" color={colors.light} size={SIZE.lg} />
|
||||
</PressableButton>
|
||||
) : null}
|
||||
{!allTags || allTags.length === 0 ? (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 200,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={50} color={colors.icon}>
|
||||
#
|
||||
</Heading>
|
||||
<Paragraph textBreakStrategy="balanced" color={colors.icon}>
|
||||
You do not have any tags.
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{tags.map((item) => (
|
||||
<TagItem
|
||||
key={item.title}
|
||||
tag={item}
|
||||
note={note}
|
||||
setNote={setNote}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</SheetWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
ManageTagsSheet.present = (note) => {
|
||||
presentSheet({
|
||||
component: (ref) => {
|
||||
return <ManageTagsSheet actionSheetRef={ref} note={note} />;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default ManageTagsSheet;
|
||||
|
||||
const TagItem = ({ tag, note, setNote }) => {
|
||||
|
||||
@@ -17,11 +17,11 @@ 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 { FlashList } from "@shopify/flash-list";
|
||||
import { NotebookType, NoteType, TopicType } from "app/utils/types";
|
||||
import React, { RefObject, useState } from "react";
|
||||
import { Platform, useWindowDimensions, View } from "react-native";
|
||||
import { ActionSheetRef } from "react-native-actions-sheet";
|
||||
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
eSendEvent,
|
||||
@@ -58,7 +58,7 @@ export const MoveNotes = ({
|
||||
}: {
|
||||
notebook: NotebookType;
|
||||
selectedTopic?: TopicType;
|
||||
fwdRef: RefObject<ActionSheetRef>;
|
||||
fwdRef: RefObject<ActionSheet>;
|
||||
}) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [currentNotebook, setCurrentNotebook] = useState(notebook);
|
||||
@@ -277,6 +277,10 @@ export const MoveNotes = ({
|
||||
)}
|
||||
|
||||
<FlashList
|
||||
nestedScrollEnabled
|
||||
onMomentumScrollEnd={() => {
|
||||
fwdRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
@@ -341,7 +345,7 @@ export const MoveNotes = ({
|
||||
|
||||
MoveNotes.present = (notebook: NotebookType, topic: TopicType) => {
|
||||
presentSheet({
|
||||
component: (ref: RefObject<ActionSheetRef>) => (
|
||||
component: (ref: RefObject<ActionSheet>) => (
|
||||
<MoveNotes fwdRef={ref} notebook={notebook} selectedTopic={topic} />
|
||||
)
|
||||
});
|
||||
|
||||
@@ -93,13 +93,7 @@ NewFeature.present = () => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!version || version === getVersion()) {
|
||||
SettingsService.set({
|
||||
version: getVersion()
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (version && version === getVersion()) return false;
|
||||
SettingsService.set({
|
||||
version: getVersion()
|
||||
});
|
||||
|
||||
@@ -18,14 +18,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useRef, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../../common/database";
|
||||
import { presentSheet, ToastEvent } from "../../../services/event-manager";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useAttachmentStore } from "../../../stores/use-attachment-store";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import {
|
||||
eClosePublishNoteDialog,
|
||||
eOpenPublishNoteDialog
|
||||
} from "../../../utils/events";
|
||||
import { openLinkInBrowser } from "../../../utils/functions";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
@@ -33,27 +41,59 @@ import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
const PublishNoteSheet = ({ note: item, update }) => {
|
||||
let passwordValue = null;
|
||||
const PublishNoteSheet = () => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const actionSheetRef = useRef();
|
||||
const loading = useAttachmentStore((state) => state.loading);
|
||||
const [selfDestruct, setSelfDestruct] = useState(false);
|
||||
const [isLocked, setIsLocked] = useState(false);
|
||||
const [note, setNote] = useState(item);
|
||||
const [note, setNote] = useState(null);
|
||||
const [publishing, setPublishing] = useState(false);
|
||||
const publishUrl =
|
||||
note &&
|
||||
`https://monograph.notesnook.com/${db?.monographs.monograph(note?.id)}`;
|
||||
const isPublished = note && db?.monographs.isPublished(note?.id);
|
||||
const pwdInput = useRef();
|
||||
const passwordValue = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenPublishNoteDialog, open);
|
||||
eSubscribeEvent(eClosePublishNoteDialog, close);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenPublishNoteDialog, open);
|
||||
eUnSubscribeEvent(eClosePublishNoteDialog, close);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const open = (item) => {
|
||||
if (!item) return;
|
||||
setNote(item);
|
||||
setPublishing(false);
|
||||
setSelfDestruct(false);
|
||||
setIsLocked(false);
|
||||
setVisible(true);
|
||||
passwordValue = null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
actionSheetRef.current?.show();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const close = () => {
|
||||
passwordValue = null;
|
||||
actionSheetRef.current?.hide();
|
||||
};
|
||||
|
||||
const publishNote = async () => {
|
||||
if (publishing) return;
|
||||
setPublishLoading(true);
|
||||
setPublishing(true);
|
||||
|
||||
try {
|
||||
if (note?.id) {
|
||||
@@ -70,7 +110,6 @@ const PublishNoteSheet = ({ note: item, update }) => {
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
setPublishLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
@@ -81,19 +120,12 @@ const PublishNoteSheet = ({ note: item, update }) => {
|
||||
});
|
||||
}
|
||||
|
||||
setPublishLoading(false);
|
||||
};
|
||||
|
||||
const setPublishLoading = (value) => {
|
||||
setPublishing(value);
|
||||
update({
|
||||
progress: value
|
||||
});
|
||||
setPublishing(false);
|
||||
};
|
||||
|
||||
const deletePublishedNote = async () => {
|
||||
if (publishing) return;
|
||||
setPublishLoading(true);
|
||||
setPublishing(true);
|
||||
try {
|
||||
if (note?.id) {
|
||||
await db.monographs.unpublish(note.id);
|
||||
@@ -105,7 +137,6 @@ const PublishNoteSheet = ({ note: item, update }) => {
|
||||
"TaggedNotes",
|
||||
"TopicNotes"
|
||||
);
|
||||
setPublishLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
@@ -116,268 +147,267 @@ const PublishNoteSheet = ({ note: item, update }) => {
|
||||
});
|
||||
}
|
||||
actionSheetRef.current?.hide();
|
||||
setPublishLoading(false);
|
||||
setPublishing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12
|
||||
return !visible ? null : (
|
||||
<SheetWrapper
|
||||
centered={false}
|
||||
fwdRef={actionSheetRef}
|
||||
closeOnTouchBackdrop={!publishing}
|
||||
gestureEnabled={!publishing}
|
||||
onClose={async () => {
|
||||
passwordValue = null;
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title={note?.title}
|
||||
paragraph={`Anyone with the link${
|
||||
isLocked ? " and password" : ""
|
||||
} of the published note can view it.`}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title={note?.title}
|
||||
paragraph={`Anyone with the link${
|
||||
isLocked ? " and password" : ""
|
||||
} of the published note can view it.`}
|
||||
/>
|
||||
|
||||
{publishing ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignContent: "center",
|
||||
height: 150,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator size={25} color={colors.accent} />
|
||||
<Paragraph
|
||||
{publishing ? (
|
||||
<View
|
||||
style={{
|
||||
textAlign: "center"
|
||||
justifyContent: "center",
|
||||
alignContent: "center",
|
||||
height: 150,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
Please wait...
|
||||
{loading && loading.current && loading.total
|
||||
? `\nDownloading attachments (${
|
||||
loading?.current / loading?.total
|
||||
})`
|
||||
: ""}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{isPublished && (
|
||||
<View
|
||||
<ActivityIndicator size={25} color={colors.accent} />
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Please wait...
|
||||
{loading && loading.current && loading.total
|
||||
? `\nDownloading attachments (${
|
||||
loading?.current / loading?.total
|
||||
})`
|
||||
: ""}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{isPublished && (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginTop: 15,
|
||||
backgroundColor: colors.nav,
|
||||
padding: 12,
|
||||
borderRadius: 5
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.sm}>Published at:</Heading>
|
||||
<Paragraph size={SIZE.xs} numberOfLines={1}>
|
||||
{publishUrl}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
onPress={async () => {
|
||||
try {
|
||||
await openLinkInBrowser(publishUrl, colors.accent);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginTop: 5,
|
||||
color: colors.pri
|
||||
}}
|
||||
>
|
||||
<Icon color={colors.accent} name="open-in-new" /> Open in
|
||||
browser
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
Clipboard.setString(publishUrl);
|
||||
ToastEvent.show({
|
||||
heading: "Note publish url copied",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
}}
|
||||
color={colors.accent}
|
||||
size={SIZE.lg}
|
||||
name="content-copy"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<Seperator />
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (publishing) return;
|
||||
setIsLocked(!isLocked);
|
||||
}}
|
||||
activeOpacity={0.9}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginTop: 15,
|
||||
backgroundColor: colors.nav,
|
||||
padding: 12,
|
||||
borderRadius: 5
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
if (publishing) return;
|
||||
setIsLocked(!isLocked);
|
||||
}}
|
||||
color={isLocked ? colors.accent : colors.icon}
|
||||
size={SIZE.lg}
|
||||
name={
|
||||
isLocked
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.sm}>Published at:</Heading>
|
||||
<Paragraph size={SIZE.xs} numberOfLines={1}>
|
||||
{publishUrl}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
onPress={async () => {
|
||||
try {
|
||||
await openLinkInBrowser(publishUrl, colors.accent);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginTop: 5,
|
||||
color: colors.pri
|
||||
}}
|
||||
>
|
||||
<Icon color={colors.accent} name="open-in-new" /> Open in
|
||||
browser
|
||||
<Heading size={SIZE.md}>Password protection</Heading>
|
||||
<Paragraph>
|
||||
Published note can only be viewed by someone with the
|
||||
password.
|
||||
</Paragraph>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
Clipboard.setString(publishUrl);
|
||||
ToastEvent.show({
|
||||
heading: "Note publish url copied",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
}}
|
||||
color={colors.accent}
|
||||
size={SIZE.lg}
|
||||
name="content-copy"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<Seperator />
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (publishing) return;
|
||||
setIsLocked(!isLocked);
|
||||
}}
|
||||
activeOpacity={0.9}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
if (publishing) return;
|
||||
setIsLocked(!isLocked);
|
||||
}}
|
||||
color={isLocked ? colors.accent : colors.icon}
|
||||
size={SIZE.lg}
|
||||
name={
|
||||
isLocked
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md}>Password protection</Heading>
|
||||
<Paragraph>
|
||||
Published note can only be viewed by someone with the password.
|
||||
</Paragraph>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setSelfDestruct(!selfDestruct);
|
||||
}}
|
||||
activeOpacity={0.9}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setSelfDestruct(!selfDestruct);
|
||||
}}
|
||||
color={selfDestruct ? colors.accent : colors.icon}
|
||||
size={SIZE.lg}
|
||||
name={
|
||||
selfDestruct
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
activeOpacity={0.9}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
setSelfDestruct(!selfDestruct);
|
||||
}}
|
||||
color={selfDestruct ? colors.accent : colors.icon}
|
||||
size={SIZE.lg}
|
||||
name={
|
||||
selfDestruct
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md}>Self destruct</Heading>
|
||||
<Paragraph>
|
||||
Published note link will be automatically deleted once it is
|
||||
viewed by someone.
|
||||
</Paragraph>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md}>Self destruct</Heading>
|
||||
<Paragraph>
|
||||
Published note link will be automatically deleted once it is
|
||||
viewed by someone.
|
||||
</Paragraph>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
{isLocked ? (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={pwdInput}
|
||||
onChangeText={(value) => (passwordValue.current = value)}
|
||||
blurOnSubmit
|
||||
secureTextEntry
|
||||
defaultValue={passwordValue.current}
|
||||
placeholder="Enter Password"
|
||||
/>
|
||||
<Seperator half />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
onPress={publishNote}
|
||||
fontSize={SIZE.md}
|
||||
width="100%"
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 10
|
||||
}}
|
||||
height={50}
|
||||
type="accent"
|
||||
title={isPublished ? "Update published note" : "Publish note"}
|
||||
/>
|
||||
>
|
||||
{isLocked ? (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={pwdInput}
|
||||
onChangeText={(value) => (passwordValue = value)}
|
||||
blurOnSubmit
|
||||
secureTextEntry
|
||||
defaultValue={passwordValue}
|
||||
placeholder="Enter Password"
|
||||
/>
|
||||
<Seperator half />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{isPublished && (
|
||||
<>
|
||||
<Seperator half />
|
||||
<Button
|
||||
onPress={deletePublishedNote}
|
||||
fontSize={SIZE.md}
|
||||
width="100%"
|
||||
height={50}
|
||||
type="error"
|
||||
title="Unpublish note"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
onPress={publishNote}
|
||||
fontSize={SIZE.md}
|
||||
width="100%"
|
||||
style={{
|
||||
marginTop: 10
|
||||
}}
|
||||
height={50}
|
||||
type="accent"
|
||||
title={isPublished ? "Update published note" : "Publish note"}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 5,
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await openLinkInBrowser(
|
||||
"https://docs.notesnook.com/monographs/",
|
||||
colors.accent
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Learn more about Notesnook Monograph
|
||||
</Paragraph>
|
||||
</View>
|
||||
{isPublished && (
|
||||
<>
|
||||
<Seperator half />
|
||||
<Button
|
||||
onPress={deletePublishedNote}
|
||||
fontSize={SIZE.md}
|
||||
width="100%"
|
||||
height={50}
|
||||
type="error"
|
||||
title="Unpublish note"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Paragraph
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 5,
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await openLinkInBrowser(
|
||||
"https://docs.notesnook.com/monographs/",
|
||||
colors.accent
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Learn more about Notesnook Monograph
|
||||
</Paragraph>
|
||||
</View>
|
||||
</SheetWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
PublishNoteSheet.present = (note) => {
|
||||
presentSheet({
|
||||
component: (ref, close, update) => (
|
||||
<PublishNoteSheet
|
||||
actionSheetRef={ref}
|
||||
close={close}
|
||||
update={update}
|
||||
note={note}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
export default PublishNoteSheet;
|
||||
|
||||
@@ -18,13 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React, { RefObject, useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { ActionSheetRef } from "react-native-actions-sheet";
|
||||
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
presentSheet,
|
||||
PresentSheetOptions
|
||||
PresentSheetOptions,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import { Reminder } from "../../../services/notifications";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
@@ -38,7 +37,7 @@ import { PressableButtonProps } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
type RelationsListProps = {
|
||||
actionSheetRef: RefObject<ActionSheetRef>;
|
||||
actionSheetRef: RefObject<ActionSheet>;
|
||||
close?: () => void;
|
||||
update?: (options: PresentSheetOptions) => void;
|
||||
item: { id: string; type: string };
|
||||
@@ -63,6 +62,8 @@ const IconsByType = {
|
||||
|
||||
export const RelationsList = ({
|
||||
actionSheetRef,
|
||||
close,
|
||||
update,
|
||||
item,
|
||||
referenceType,
|
||||
relationType,
|
||||
@@ -74,6 +75,7 @@ export const RelationsList = ({
|
||||
const [items, setItems] = useState<Reminder[]>([]);
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const hasNoRelations = !items || items.length === 0;
|
||||
|
||||
useEffect(() => {
|
||||
setItems(
|
||||
db.relations?.[relationType]?.(
|
||||
@@ -121,7 +123,6 @@ export const RelationsList = ({
|
||||
) : (
|
||||
<List
|
||||
listData={items}
|
||||
ScrollComponent={FlashList}
|
||||
loading={false}
|
||||
type={referenceType}
|
||||
headerProps={null}
|
||||
|
||||
@@ -18,8 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import dayjs from "dayjs";
|
||||
import React, { RefObject } from "react";
|
||||
import { View } from "react-native";
|
||||
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
@@ -36,7 +36,7 @@ import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
type ReminderSheetProps = {
|
||||
actionSheetRef: RefObject<ActionSheetRef>;
|
||||
actionSheetRef: RefObject<ActionSheet>;
|
||||
close?: () => void;
|
||||
update?: (options: PresentSheetOptions) => void;
|
||||
reminder?: Reminder;
|
||||
|
||||
@@ -17,8 +17,14 @@ 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, { RefObject, useRef, useState } from "react";
|
||||
import { Platform, TextInput, View } from "react-native";
|
||||
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
|
||||
import {
|
||||
Platform,
|
||||
ScrollView,
|
||||
TextInput,
|
||||
useWindowDimensions,
|
||||
View
|
||||
} from "react-native";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import DateTimePickerModal from "react-native-modal-datetime-picker";
|
||||
import {
|
||||
presentSheet,
|
||||
@@ -38,13 +44,13 @@ import Notifications, { Reminder } from "../../../services/notifications";
|
||||
import PremiumService from "../../../services/premium";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { NoteType } from "../../../utils/types";
|
||||
import { Dialog } from "../../dialog";
|
||||
import { ReminderTime } from "../../ui/reminder-time";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { NoteType } from "../../../utils/types";
|
||||
import { Dialog } from "../../dialog";
|
||||
|
||||
type ReminderSheetProps = {
|
||||
actionSheetRef: RefObject<ActionSheetRef>;
|
||||
actionSheetRef: RefObject<ActionSheet>;
|
||||
close?: (ctx?: string) => void;
|
||||
update?: (options: PresentSheetOptions) => void;
|
||||
reminder?: Reminder;
|
||||
@@ -111,15 +117,13 @@ export default function ReminderSheet({
|
||||
>(reminder?.priority || SettingsService.get().reminderNotificationMode);
|
||||
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
|
||||
const [repeatFrequency, setRepeatFrequency] = useState(1);
|
||||
const title = useRef<string | undefined>(reminder?.title);
|
||||
const details = useRef<string | undefined>(reminder?.description);
|
||||
const titleRef = useRef<TextInput>(null);
|
||||
const { height } = useWindowDimensions();
|
||||
const referencedItem = reference
|
||||
? (db.notes?.note(reference.id)?.data as NoteType)
|
||||
: null;
|
||||
const title = useRef<string | undefined>(
|
||||
reminder?.title || referencedItem?.title
|
||||
);
|
||||
const details = useRef<string | undefined>(reminder?.description);
|
||||
const titleRef = useRef<TextInput>(null);
|
||||
const timer = useRef<NodeJS.Timeout>();
|
||||
|
||||
const showDatePicker = () => {
|
||||
setDatePickerVisibility(true);
|
||||
@@ -130,10 +134,9 @@ export default function ReminderSheet({
|
||||
};
|
||||
|
||||
const handleConfirm = (date: Date) => {
|
||||
timer.current = setTimeout(() => {
|
||||
hideDatePicker();
|
||||
setDate(date);
|
||||
}, 50);
|
||||
hideDatePicker();
|
||||
setDate(date);
|
||||
console.log(date);
|
||||
};
|
||||
function nth(n: number) {
|
||||
return (
|
||||
@@ -230,8 +233,13 @@ export default function ReminderSheet({
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Dialog context="local" />
|
||||
<ScrollView keyboardShouldPersistTaps="always">
|
||||
<Dialog context="local"/>
|
||||
<ScrollView
|
||||
onScrollEndDrag={() => actionSheetRef.current?.handleChildScrollEnd()}
|
||||
style={{
|
||||
maxHeight: height * 0.85
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={titleRef}
|
||||
defaultValue={reminder?.title || referencedItem?.title}
|
||||
@@ -241,9 +249,7 @@ export default function ReminderSheet({
|
||||
/>
|
||||
|
||||
<Input
|
||||
defaultValue={
|
||||
reminder ? reminder?.description : referencedItem?.headline
|
||||
}
|
||||
defaultValue={reminder ? reminder?.description : referencedItem?.headline}
|
||||
placeholder="Add a quick note"
|
||||
onChangeText={(text) => (details.current = text)}
|
||||
containerStyle={{
|
||||
@@ -434,12 +440,6 @@ export default function ReminderSheet({
|
||||
|
||||
<DatePicker
|
||||
date={date}
|
||||
minimumDate={
|
||||
dayjs(date).subtract(3, "months").isBefore(dayjs())
|
||||
? dayjs().toDate()
|
||||
: dayjs(date).subtract(3, "months").toDate()
|
||||
}
|
||||
maximumDate={dayjs(date).add(3, "months").toDate()}
|
||||
onDateChange={handleConfirm}
|
||||
textColor={colors.night ? "#ffffff" : "#000000"}
|
||||
fadeToColor={colors.bg}
|
||||
@@ -561,16 +561,16 @@ export default function ReminderSheet({
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
title="Save"
|
||||
type="accent"
|
||||
fontSize={SIZE.md}
|
||||
onPress={saveReminder}
|
||||
/>
|
||||
</ScrollView>
|
||||
<Button
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
title="Save"
|
||||
type="accent"
|
||||
fontSize={SIZE.md}
|
||||
onPress={saveReminder}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -582,7 +582,6 @@ ReminderSheet.present = (
|
||||
) => {
|
||||
presentSheet({
|
||||
context: isSheet ? "local" : undefined,
|
||||
enableGesturesInScrollView: true,
|
||||
component: (ref, close, update) => (
|
||||
<ReminderSheet
|
||||
actionSheetRef={ref}
|
||||
|
||||
@@ -18,10 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { EVENTS } from "@notesnook/core/common";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, { createRef, useCallback, useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import DocumentPicker from "react-native-document-picker";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { db } from "../../../common/database";
|
||||
import storage from "../../../common/database/storage";
|
||||
@@ -35,7 +35,7 @@ import { initialize } from "../../../stores";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { eCloseRestoreDialog, eOpenRestoreDialog } from "../../../utils/events";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { timeConverter } from "../../../utils/time";
|
||||
import { sleep, timeConverter } from "../../../utils/time";
|
||||
import { Dialog } from "../../dialog";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
@@ -44,18 +44,12 @@ import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
const actionSheetRef = createRef();
|
||||
let RNFetchBlob;
|
||||
const RestoreDataSheet = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [restoring, setRestoring] = useState(false);
|
||||
const sheet = useRef();
|
||||
useEffect(() => {
|
||||
const open = async () => {
|
||||
setVisible(true);
|
||||
setTimeout(() => {
|
||||
sheet.current?.show();
|
||||
}, 1);
|
||||
};
|
||||
eSubscribeEvent(eOpenRestoreDialog, open);
|
||||
eSubscribeEvent(eCloseRestoreDialog, close);
|
||||
return () => {
|
||||
@@ -64,15 +58,21 @@ const RestoreDataSheet = () => {
|
||||
};
|
||||
}, [close]);
|
||||
|
||||
const open = async () => {
|
||||
setVisible(true);
|
||||
await sleep(30);
|
||||
actionSheetRef.current?.setModalVisible(true);
|
||||
};
|
||||
|
||||
const close = useCallback(() => {
|
||||
if (restoring) {
|
||||
showIsWorking();
|
||||
return;
|
||||
}
|
||||
sheet.current?.hide();
|
||||
actionSheetRef.current?.setModalVisible(false);
|
||||
setTimeout(() => {
|
||||
setVisible(false);
|
||||
}, 150);
|
||||
}, 300);
|
||||
}, [restoring]);
|
||||
|
||||
const showIsWorking = () => {
|
||||
@@ -86,19 +86,15 @@ const RestoreDataSheet = () => {
|
||||
|
||||
return !visible ? null : (
|
||||
<SheetWrapper
|
||||
fwdRef={sheet}
|
||||
fwdRef={actionSheetRef}
|
||||
gestureEnabled={!restoring}
|
||||
closeOnTouchBackdrop={!restoring}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
close();
|
||||
}}
|
||||
onClose={close}
|
||||
>
|
||||
<RestoreDataComponent
|
||||
close={close}
|
||||
restoring={restoring}
|
||||
setRestoring={setRestoring}
|
||||
actionSheetRef={sheet}
|
||||
/>
|
||||
<Toast context="local" />
|
||||
</SheetWrapper>
|
||||
@@ -113,6 +109,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [backupDirectoryAndroid, setBackupDirectoryAndroid] = useState(false);
|
||||
const [progress, setProgress] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = db.eventManager.subscribe(
|
||||
EVENTS.migrationProgress,
|
||||
@@ -126,9 +123,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
checkBackups();
|
||||
}, 300);
|
||||
checkBackups();
|
||||
}, []);
|
||||
|
||||
const restore = async (item) => {
|
||||
@@ -367,6 +362,10 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
</View>
|
||||
<Seperator half />
|
||||
<FlatList
|
||||
nestedScrollEnabled
|
||||
onMomentumScrollEnd={() => {
|
||||
actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
ListEmptyComponent={
|
||||
!restoring ? (
|
||||
loading ? (
|
||||
|
||||
@@ -100,7 +100,7 @@ const Sort = ({ type, screen }) => {
|
||||
height={25}
|
||||
iconPosition="right"
|
||||
fontSize={SIZE.sm - 1}
|
||||
type="transparent"
|
||||
type="grayBg"
|
||||
buttonType={{
|
||||
text: colors.accent
|
||||
}}
|
||||
|
||||
@@ -1,451 +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 qclone from "qclone";
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { Animated, Dimensions, View, RefreshControl } from "react-native";
|
||||
import ActionSheet, {
|
||||
ActionSheetRef,
|
||||
FlatList
|
||||
} from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import { IconButton } from "../../../components/ui/icon-button";
|
||||
import { PressableButton } from "../../../components/ui/pressable";
|
||||
import Paragraph from "../../../components/ui/typography/paragraph";
|
||||
import { TopicNotes } from "../../../screens/notes/topic-notes";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import useNavigationStore, {
|
||||
NotebookScreenParams
|
||||
} from "../../../stores/use-navigation-store";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import {
|
||||
eOnNewTopicAdded,
|
||||
eOnTopicSheetUpdate,
|
||||
eOpenAddTopicDialog
|
||||
} from "../../../utils/events";
|
||||
import { normalize, SIZE } from "../../../utils/size";
|
||||
import { NotebookType, TopicType } from "../../../utils/types";
|
||||
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { openEditor } from "../../../screens/notes/common";
|
||||
import { getTotalNotes, history } from "../../../utils";
|
||||
import { Properties } from "../../properties";
|
||||
import { deleteItems } from "../../../utils/functions";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import Config from "react-native-config";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
|
||||
export const TopicsSheet = () => {
|
||||
const currentScreen = useNavigationStore((state) => state.currentScreen);
|
||||
const canShow =
|
||||
currentScreen.name === "Notebook" || currentScreen.name === "TopicNotes";
|
||||
const [notebook, setNotebook] = useState(
|
||||
canShow
|
||||
? db.notebooks?.notebook(
|
||||
currentScreen?.notebookId || currentScreen?.id || ""
|
||||
)?.data
|
||||
: null
|
||||
);
|
||||
const [selection, setSelection] = useState<TopicType[]>([]);
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const ref = useRef<ActionSheetRef>(null);
|
||||
const [topics, setTopics] = useState(notebook ? qclone(notebook.topics) : []);
|
||||
const [animations] = useState({
|
||||
translate: new Animated.Value(0),
|
||||
display: new Animated.Value(-5000),
|
||||
opacity: new Animated.Value(0)
|
||||
});
|
||||
const onRequestUpdate = React.useCallback(
|
||||
(data?: NotebookScreenParams) => {
|
||||
if (!canShow) return;
|
||||
if (!data) data = { item: notebook } as NotebookScreenParams;
|
||||
const _notebook = db.notebooks?.notebook(data.item?.id)
|
||||
?.data as NotebookType;
|
||||
if (_notebook) {
|
||||
setNotebook(_notebook);
|
||||
setTopics(qclone(_notebook.topics));
|
||||
}
|
||||
},
|
||||
[notebook, canShow]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const onTopicUpdate = () => {
|
||||
onRequestUpdate();
|
||||
};
|
||||
eSubscribeEvent(eOnTopicSheetUpdate, onTopicUpdate);
|
||||
eSubscribeEvent(eOnNewTopicAdded, onRequestUpdate);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOnTopicSheetUpdate, onRequestUpdate);
|
||||
eUnSubscribeEvent(eOnNewTopicAdded, onTopicUpdate);
|
||||
};
|
||||
}, [onRequestUpdate]);
|
||||
|
||||
const PLACEHOLDER_DATA = {
|
||||
heading: "Topics",
|
||||
paragraph: "You have not added any topics yet.",
|
||||
button: "Add first topic",
|
||||
action: () => {
|
||||
eSendEvent(eOpenAddTopicDialog, { notebookId: notebook.id });
|
||||
},
|
||||
loading: "Loading notebook topics"
|
||||
};
|
||||
|
||||
const renderTopic = ({ item, index }: { item: TopicType; index: number }) => (
|
||||
<TopicItem item={item} index={index} />
|
||||
);
|
||||
|
||||
const selectionContext = {
|
||||
selection: selection,
|
||||
enabled,
|
||||
setEnabled,
|
||||
toggleSelection: (item: TopicType) => {
|
||||
setSelection((state) => {
|
||||
const selection = [...state];
|
||||
const index = selection.findIndex(
|
||||
(selected) => selected.id === item.id
|
||||
);
|
||||
if (index > -1) {
|
||||
selection.splice(index, 1);
|
||||
if (selection.length === 0) {
|
||||
setEnabled(false);
|
||||
}
|
||||
return selection;
|
||||
}
|
||||
selection.push(item);
|
||||
return selection;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (canShow) {
|
||||
const isTopic = currentScreen.name === "TopicNotes";
|
||||
const id = isTopic ? currentScreen?.notebookId : currentScreen?.id;
|
||||
if (!ref.current?.isOpen()) {
|
||||
animations.display.setValue(5000);
|
||||
animations.opacity.setValue(0);
|
||||
}
|
||||
if (id) {
|
||||
onRequestUpdate({
|
||||
item: db.notebooks?.notebook(id).data
|
||||
} as any);
|
||||
}
|
||||
ref.current?.show();
|
||||
} else {
|
||||
ref.current?.hide();
|
||||
}
|
||||
}, [
|
||||
animations.display,
|
||||
animations.opacity,
|
||||
canShow,
|
||||
currentScreen?.id,
|
||||
currentScreen.name,
|
||||
currentScreen?.notebookId,
|
||||
onRequestUpdate
|
||||
]);
|
||||
|
||||
return (
|
||||
<ActionSheet
|
||||
ref={ref}
|
||||
isModal={false}
|
||||
containerStyle={{
|
||||
maxHeight: 400,
|
||||
borderTopRightRadius: 15,
|
||||
borderTopLeftRadius: 15,
|
||||
backgroundColor: colors.bg,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
borderBottomWidth: 0
|
||||
}}
|
||||
closable={!canShow}
|
||||
elevation={10}
|
||||
indicatorStyle={{
|
||||
width: 100,
|
||||
backgroundColor: colors.nav
|
||||
}}
|
||||
keyboardHandlerEnabled={false}
|
||||
snapPoints={Config.isTesting === "true" ? [60, 100] : [15, 60, 100]}
|
||||
initialSnapIndex={0}
|
||||
backgroundInteractionEnabled
|
||||
onChange={(position, height) => {
|
||||
animations.translate.setValue(position - 60);
|
||||
const h = Dimensions.get("window").height;
|
||||
const minPos = h - height;
|
||||
if (position - 100 < minPos || !canShow) {
|
||||
animations.display.setValue(5000);
|
||||
animations.opacity.setValue(0);
|
||||
} else {
|
||||
animations.display.setValue(0);
|
||||
setTimeout(() => {
|
||||
animations.opacity.setValue(1);
|
||||
}, 300);
|
||||
}
|
||||
}}
|
||||
gestureEnabled
|
||||
ExtraOverlayComponent={
|
||||
<Animated.View
|
||||
style={{
|
||||
top: animations.translate,
|
||||
position: "absolute",
|
||||
right: 12,
|
||||
opacity: animations.opacity,
|
||||
transform: [
|
||||
{
|
||||
translateY: animations.display
|
||||
}
|
||||
]
|
||||
}}
|
||||
>
|
||||
<PressableButton
|
||||
testID={notesnook.buttons.add}
|
||||
type="accent"
|
||||
accentColor={"accent"}
|
||||
accentText="light"
|
||||
onPress={openEditor}
|
||||
customStyle={{
|
||||
borderRadius: 100
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: normalize(60),
|
||||
width: normalize(60)
|
||||
}}
|
||||
>
|
||||
<Icon name="plus" color="white" size={SIZE.xxl} />
|
||||
</View>
|
||||
</PressableButton>
|
||||
</Animated.View>
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
maxHeight: 400,
|
||||
height: 400,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 12,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs} color={colors.icon}>
|
||||
TOPICS
|
||||
</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
{enabled ? (
|
||||
<IconButton
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
onPress={async () => {
|
||||
//@ts-ignore
|
||||
history.selectedItemsList = selection;
|
||||
presentDialog({
|
||||
title: `Delete ${
|
||||
selection.length > 1 ? "topics" : "topics"
|
||||
}`,
|
||||
paragraph: `Are you sure you want to delete ${
|
||||
selection.length > 1 ? "these topicss?" : "this topics?"
|
||||
}`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
positivePress: async () => {
|
||||
await deleteItems();
|
||||
history.selectedItemsList = [];
|
||||
setEnabled(false);
|
||||
setSelection([]);
|
||||
},
|
||||
positiveType: "errorShade"
|
||||
});
|
||||
return;
|
||||
}}
|
||||
color={colors.pri}
|
||||
tooltipText="Move to trash"
|
||||
tooltipPosition={1}
|
||||
name="delete"
|
||||
size={22}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
name="plus"
|
||||
onPress={PLACEHOLDER_DATA.action}
|
||||
testID="add-topic-button"
|
||||
color={colors.pri}
|
||||
size={22}
|
||||
customStyle={{
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<SelectionContext.Provider value={selectionContext}>
|
||||
<FlatList
|
||||
data={topics}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={false}
|
||||
onRefresh={() => {
|
||||
onRequestUpdate();
|
||||
}}
|
||||
colors={[colors.accent]}
|
||||
progressBackgroundColor={colors.bg}
|
||||
/>
|
||||
}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderTopic}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 300
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.icon}>No topics</Paragraph>
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={<View style={{ height: 50 }} />}
|
||||
/>
|
||||
</SelectionContext.Provider>
|
||||
</View>
|
||||
</ActionSheet>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectionContext = createContext<{
|
||||
selection: TopicType[];
|
||||
enabled: boolean;
|
||||
setEnabled: (value: boolean) => void;
|
||||
toggleSelection: (item: TopicType) => void;
|
||||
}>({
|
||||
selection: [],
|
||||
enabled: false,
|
||||
setEnabled: (value: boolean) => {},
|
||||
toggleSelection: (item: TopicType) => {}
|
||||
});
|
||||
const useSelection = () => useContext(SelectionContext);
|
||||
|
||||
const TopicItem = ({ item, index }: { item: TopicType; index: number }) => {
|
||||
const screen = useNavigationStore((state) => state.currentScreen);
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const selection = useSelection();
|
||||
const isSelected =
|
||||
selection.selection.findIndex((selected) => selected.id === item.id) > -1;
|
||||
const isFocused = screen.id === item.id;
|
||||
const notesCount = getTotalNotes(item);
|
||||
|
||||
return (
|
||||
<PressableButton
|
||||
type={isSelected || isFocused ? "grayBg" : "transparent"}
|
||||
onLongPress={() => {
|
||||
if (selection.enabled) return;
|
||||
selection.setEnabled(true);
|
||||
selection.toggleSelection(item);
|
||||
}}
|
||||
testID={`topic-sheet-item-${index}`}
|
||||
onPress={() => {
|
||||
if (selection.enabled) {
|
||||
selection.toggleSelection(item);
|
||||
return;
|
||||
}
|
||||
TopicNotes.navigate(item, true);
|
||||
}}
|
||||
customStyle={{
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 0
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{selection.enabled ? (
|
||||
<IconButton
|
||||
size={SIZE.lg}
|
||||
color={isSelected ? colors.accent : colors.icon}
|
||||
name={
|
||||
isSelected
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<Paragraph size={SIZE.sm}>
|
||||
{item.title}{" "}
|
||||
{notesCount ? (
|
||||
<Paragraph size={SIZE.xs} color={colors.icon}>
|
||||
{notesCount}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</Paragraph>
|
||||
</View>
|
||||
<IconButton
|
||||
name="dots-horizontal"
|
||||
customStyle={{
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
testID={notesnook.ids.notebook.menu}
|
||||
onPress={() => {
|
||||
Properties.present(item);
|
||||
}}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
top={0}
|
||||
color={colors.pri}
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
</PressableButton>
|
||||
);
|
||||
};
|
||||
@@ -19,10 +19,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Linking, View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import { checkVersion } from "react-native-check-version";
|
||||
import Config from "react-native-config";
|
||||
import deviceInfoModule from "react-native-device-info";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { useThemeStore } from "../../../stores/use-theme-store";
|
||||
import { STORE_LINK } from "../../../utils/constants";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
@@ -148,6 +148,9 @@ export const Update = ({ version: appVersion, fwdRef }) => {
|
||||
<Seperator />
|
||||
<ScrollView
|
||||
nestedScrollEnabled={true}
|
||||
onMomentumScrollEnd={() => {
|
||||
fwdRef?.current?.handleChildScrollEnd();
|
||||
}}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
|
||||
@@ -37,8 +37,7 @@ const SheetWrapper = ({
|
||||
onHasReachedTop,
|
||||
keyboardMode,
|
||||
overlay,
|
||||
overlayOpacity = 0.3,
|
||||
enableGesturesInScrollView = false
|
||||
overlayOpacity = 0.3
|
||||
}) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
@@ -60,8 +59,8 @@ const SheetWrapper = ({
|
||||
zIndex: 10,
|
||||
paddingTop: 5,
|
||||
paddingBottom: 0,
|
||||
borderTopRightRadius: 15,
|
||||
borderTopLeftRadius: 15,
|
||||
borderTopRightRadius: 20,
|
||||
borderTopLeftRadius: 20,
|
||||
alignSelf: "center",
|
||||
borderBottomRightRadius: 0,
|
||||
borderBottomLeftRadius: 0
|
||||
@@ -85,8 +84,7 @@ const SheetWrapper = ({
|
||||
backdrop: "sheet-backdrop"
|
||||
}}
|
||||
indicatorStyle={{
|
||||
width: 100,
|
||||
backgroundColor: colors.nav
|
||||
width: 100
|
||||
}}
|
||||
drawUnderStatusBar={false}
|
||||
containerStyle={style}
|
||||
@@ -100,9 +98,8 @@ const SheetWrapper = ({
|
||||
indicatorColor={colors.nav}
|
||||
onOpen={_onOpen}
|
||||
keyboardDismissMode="none"
|
||||
enableGesturesInScrollView={enableGesturesInScrollView}
|
||||
defaultOverlayOpacity={overlayOpacity}
|
||||
overlayColor={pitchBlack ? "#585858" : "#2b2b2b"}
|
||||
overlayColor={pitchBlack ? "#585858" : "#000000"}
|
||||
keyboardShouldPersistTaps="always"
|
||||
ExtraOverlayComponent={
|
||||
<>
|
||||
|
||||
@@ -46,7 +46,6 @@ export default function Tag({
|
||||
marginLeft: 2,
|
||||
marginTop: -10,
|
||||
height: 20,
|
||||
justifyContent: "center",
|
||||
...style
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -22,15 +22,10 @@ import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import Share from "react-native-share";
|
||||
import { db } from "../common/database";
|
||||
import { AttachmentDialog } from "../components/attachments";
|
||||
import { presentDialog } from "../components/dialog/functions";
|
||||
import NoteHistory from "../components/note-history";
|
||||
import { AddNotebookSheet } from "../components/sheets/add-notebook";
|
||||
import MoveNoteSheet from "../components/sheets/add-to";
|
||||
import ExportNotesSheet from "../components/sheets/export-notes";
|
||||
import { MoveNotes } from "../components/sheets/move-notes/movenote";
|
||||
import PublishNoteSheet from "../components/sheets/publish-note";
|
||||
import { RelationsList } from "../components/sheets/relations-list/index";
|
||||
import ReminderSheet from "../components/sheets/reminder";
|
||||
import {
|
||||
eSendEvent,
|
||||
@@ -45,16 +40,24 @@ import Notifications from "../services/notifications";
|
||||
import { useEditorStore } from "../stores/use-editor-store";
|
||||
import { useMenuStore } from "../stores/use-menu-store";
|
||||
import useNavigationStore from "../stores/use-navigation-store";
|
||||
import { useRelationStore } from "../stores/use-relation-store";
|
||||
import { useSelectionStore } from "../stores/use-selection-store";
|
||||
import { useTagStore } from "../stores/use-tag-store";
|
||||
import { useThemeStore } from "../stores/use-theme-store";
|
||||
import { useUserStore } from "../stores/use-user-store";
|
||||
import { toTXT } from "../utils";
|
||||
import { toggleDarkMode } from "../utils/color-scheme/utils";
|
||||
import { eOpenAddTopicDialog, eOpenLoginDialog } from "../utils/events";
|
||||
import {
|
||||
eOpenAddNotebookDialog,
|
||||
eOpenAddTopicDialog,
|
||||
eOpenAttachmentsDialog,
|
||||
eOpenLoginDialog,
|
||||
eOpenMoveNoteDialog,
|
||||
eOpenPublishNoteDialog
|
||||
} from "../utils/events";
|
||||
import { deleteItems } from "../utils/functions";
|
||||
import { sleep } from "../utils/time";
|
||||
import { RelationsList } from "../components/sheets/relations-list/index";
|
||||
import { useRelationStore } from "../stores/use-relation-store";
|
||||
|
||||
export const useActions = ({ close = () => null, item }) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
@@ -102,15 +105,6 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isNoteInNotebook = () => {
|
||||
const currentScreen = useNavigationStore.getState().currentScreen;
|
||||
if (item.type !== "note" || currentScreen.name !== "Notebook") return;
|
||||
|
||||
return !!db.relations
|
||||
.to(item, "notebook")
|
||||
.find((notebook) => notebook.id === currentScreen.id);
|
||||
};
|
||||
|
||||
const onUpdate = useCallback(
|
||||
async (type) => {
|
||||
if (type === "unpin") {
|
||||
@@ -135,9 +129,12 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
}
|
||||
|
||||
function addTo() {
|
||||
close();
|
||||
clearSelection(true);
|
||||
setSelectedItem(item);
|
||||
MoveNoteSheet.present(item);
|
||||
setTimeout(() => {
|
||||
eSendEvent(eOpenMoveNoteDialog, item);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
async function addToFavorites() {
|
||||
@@ -273,7 +270,9 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
PublishNoteSheet.present(item);
|
||||
close();
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenPublishNoteDialog, item);
|
||||
}
|
||||
|
||||
const checkNoteSynced = () => {
|
||||
@@ -515,22 +514,6 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
close();
|
||||
}
|
||||
|
||||
async function removeNoteFromNotebook() {
|
||||
const currentScreen = useNavigationStore.getState().currentScreen;
|
||||
if (currentScreen.name !== "Notebook") return;
|
||||
await db.relations.unlink({ type: "notebook", id: currentScreen.id }, item);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes",
|
||||
"Notebook",
|
||||
"Notebooks"
|
||||
);
|
||||
close();
|
||||
}
|
||||
|
||||
async function deleteTrashItem() {
|
||||
if (!checkNoteSynced()) return;
|
||||
close();
|
||||
@@ -555,16 +538,22 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
}
|
||||
|
||||
async function openHistory() {
|
||||
close();
|
||||
await sleep(300);
|
||||
presentSheet({
|
||||
component: (ref) => <NoteHistory fwdRef={ref} note={item} />
|
||||
});
|
||||
}
|
||||
|
||||
async function showAttachments() {
|
||||
AttachmentDialog.present();
|
||||
close();
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenAttachmentsDialog, item);
|
||||
}
|
||||
|
||||
async function exportNote() {
|
||||
close();
|
||||
await sleep(300);
|
||||
ExportNotesSheet.present([item]);
|
||||
}
|
||||
|
||||
@@ -658,6 +647,8 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
title: "Add notes",
|
||||
icon: "plus",
|
||||
func: async () => {
|
||||
close();
|
||||
await sleep(500);
|
||||
MoveNotes.present(db.notebooks.notebook(item.notebookId).data, item);
|
||||
}
|
||||
},
|
||||
@@ -698,7 +689,9 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
title: "Edit notebook",
|
||||
icon: "square-edit-outline",
|
||||
func: async () => {
|
||||
AddNotebookSheet.present(item);
|
||||
close();
|
||||
await sleep(300);
|
||||
eSendEvent(eOpenAddNotebookDialog, item);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -784,6 +777,8 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
title: "Edit reminder",
|
||||
icon: "pencil",
|
||||
func: async () => {
|
||||
close();
|
||||
await sleep(300);
|
||||
ReminderSheet.present(item);
|
||||
},
|
||||
close: false
|
||||
@@ -794,6 +789,7 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
title: "Reminders",
|
||||
icon: "clock-outline",
|
||||
func: async () => {
|
||||
close();
|
||||
RelationsList.present({
|
||||
reference: item,
|
||||
referenceType: "reminder",
|
||||
@@ -853,13 +849,6 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
icon: "minus-circle-outline",
|
||||
func: removeNoteFromTopic
|
||||
},
|
||||
{
|
||||
id: "remove-from-notebook",
|
||||
title: "Remove from notebook",
|
||||
hidden: !isNoteInNotebook(),
|
||||
icon: "minus-circle-outline",
|
||||
func: removeNoteFromNotebook
|
||||
},
|
||||
{
|
||||
id: "trash",
|
||||
title:
|
||||
|
||||
@@ -24,7 +24,6 @@ import { SafeAreaView } from "react-native";
|
||||
import Container from "../components/container";
|
||||
import DelayLayout from "../components/delay-layout";
|
||||
import Intro from "../components/intro";
|
||||
import { TopicsSheet } from "../components/sheets/topic-sheet";
|
||||
import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets";
|
||||
import { hideAllTooltips } from "../hooks/use-tooltip";
|
||||
import Favorites from "../screens/favorites";
|
||||
@@ -199,7 +198,6 @@ const _NavigationStack = () => {
|
||||
<NavigationContainer onStateChange={onStateChange} ref={rootNavigatorRef}>
|
||||
<Tabs />
|
||||
</NavigationContainer>
|
||||
<TopicsSheet />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -235,17 +235,14 @@ const _TabsHolder = () => {
|
||||
let needsUpdate = current !== deviceMode;
|
||||
|
||||
if (fullscreen && current !== "mobile") {
|
||||
// Runs after size is set via state.
|
||||
setTimeout(() => {
|
||||
editorRef.current?.setNativeProps({
|
||||
style: {
|
||||
width: size.width,
|
||||
zIndex: 999,
|
||||
paddingHorizontal:
|
||||
current === "smallTablet" ? size.width * 0 : size.width * 0.15
|
||||
}
|
||||
});
|
||||
}, 1);
|
||||
editorRef.current?.setNativeProps({
|
||||
style: {
|
||||
width: size.width,
|
||||
zIndex: 999,
|
||||
paddingHorizontal:
|
||||
current === "smallTablet" ? size.width * 0 : size.width * 0.15
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (fullscreen) eSendEvent(eCloseFullscreenEditor, current);
|
||||
editorRef.current?.setNativeProps({
|
||||
@@ -284,8 +281,10 @@ const _TabsHolder = () => {
|
||||
!editorState().movedAway &&
|
||||
useEditorStore.getState().currentEditingNote
|
||||
) {
|
||||
console.log("editor");
|
||||
tabBarRef.current?.goToIndex(2, false);
|
||||
} else {
|
||||
console.log("home");
|
||||
tabBarRef.current?.goToIndex(1, false);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
"main": "./App.js",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"react": "18.0.0",
|
||||
"react-native": "0.69.7",
|
||||
"@flyerhq/react-native-link-preview": "^1.6.0",
|
||||
"@mdi/js": "^6.7.96",
|
||||
"absolutify": "^0.1.0",
|
||||
@@ -15,7 +13,7 @@
|
||||
"html-to-text": "8.1.0",
|
||||
"phone": "^3.1.14",
|
||||
"qclone": "^1.2.0",
|
||||
"react-native-actions-sheet": "^0.9.0-alpha.11",
|
||||
"react-native-actions-sheet": "^0.7.2",
|
||||
"react-native-check-version": "https://github.com/flexible-agency/react-native-check-version",
|
||||
"react-native-drax": "^0.10.2",
|
||||
"react-native-image-zoom-viewer": "^3.0.1",
|
||||
@@ -30,6 +28,7 @@
|
||||
"zustand": "^3.6.0",
|
||||
"fflate": "^0.7.3",
|
||||
"timeago.js": "4.0.2"
|
||||
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ const EditorOverlay = ({ editorId = "", editor }) => {
|
||||
setTimeout(() => {
|
||||
translateValue.value = 6000;
|
||||
}, 500);
|
||||
}, 0);
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
[opacity, translateValue]
|
||||
|
||||
@@ -28,5 +28,5 @@ const EditorMobileSourceUrl =
|
||||
* The url should be something like this: http://192.168.100.126:3000/index.html
|
||||
*/
|
||||
export const EDITOR_URI = __DEV__
|
||||
? EditorMobileSourceUrl
|
||||
? "http://192.168.8.103:3000/index.html"
|
||||
: EditorMobileSourceUrl;
|
||||
|
||||
@@ -250,10 +250,7 @@ const handleImageResponse = async (response, options) => {
|
||||
if (isPng || isJpeg) {
|
||||
b64 =
|
||||
`data:${image.type};base64, ` +
|
||||
(await compressToBase64(
|
||||
Platform.OS === "ios" ? "file://" + image.uri : image.uri,
|
||||
isPng ? "PNG" : "JPEG"
|
||||
));
|
||||
(await compressToBase64(image.uri, isPng ? "PNG" : "JPEG"));
|
||||
}
|
||||
|
||||
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
} from "react-native";
|
||||
import { WebViewMessageEvent } from "react-native-webview";
|
||||
import { db } from "../../../common/database";
|
||||
import ManageTagsSheet from "../../../components/sheets/manage-tags";
|
||||
import ImagePreview from "../../../components/image-preview";
|
||||
import { RelationsList } from "../../../components/sheets/relations-list";
|
||||
import ReminderSheet from "../../../components/sheets/reminder";
|
||||
import useKeyboard from "../../../hooks/use-keyboard";
|
||||
@@ -52,7 +52,8 @@ import {
|
||||
eOpenFullscreenEditor,
|
||||
eOpenLoginDialog,
|
||||
eOpenPremiumDialog,
|
||||
eOpenPublishNoteDialog
|
||||
eOpenPublishNoteDialog,
|
||||
eOpenTagsDialog
|
||||
} from "../../../utils/events";
|
||||
import { openLinkInBrowser } from "../../../utils/functions";
|
||||
import { tabBarRef } from "../../../utils/global-refs";
|
||||
@@ -326,7 +327,7 @@ export const useEditorEvents = (
|
||||
});
|
||||
return;
|
||||
}
|
||||
ManageTagsSheet.present(editor.note.current);
|
||||
eSendEvent(eOpenTagsDialog, editor.note.current);
|
||||
break;
|
||||
case EventTypes.tag:
|
||||
if (editorMessage.value) {
|
||||
|
||||
@@ -65,6 +65,7 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
|
||||
onBlur: () => false,
|
||||
delay: SettingsService.get().homepage === route.name ? 1 : -1
|
||||
});
|
||||
|
||||
return (
|
||||
<DelayLayout wait={loading} delay={500}>
|
||||
<List
|
||||
|
||||
@@ -16,9 +16,12 @@ 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 { groupArray } from "@notesnook/core/utils/grouping";
|
||||
import qclone from "qclone";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { db } from "../../common/database";
|
||||
import { FloatingButton } from "../../components/container/floating-button";
|
||||
import DelayLayout from "../../components/delay-layout";
|
||||
import List from "../../components/list";
|
||||
import { NotebookHeader } from "../../components/list-items/headers/notebook-header";
|
||||
@@ -33,29 +36,28 @@ import SearchService from "../../services/search";
|
||||
import useNavigationStore, {
|
||||
NotebookScreenParams
|
||||
} from "../../stores/use-navigation-store";
|
||||
import { eOnNewTopicAdded, eOpenAddNotebookDialog } from "../../utils/events";
|
||||
import {
|
||||
eOnNewTopicAdded,
|
||||
eOpenAddNotebookDialog,
|
||||
eOpenAddTopicDialog
|
||||
} from "../../utils/events";
|
||||
import { NotebookType } from "../../utils/types";
|
||||
import { openEditor, setOnFirstSave } from "../notes/common";
|
||||
const Notebook = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
||||
const [notes, setNotes] = useState(
|
||||
const [topics, setTopics] = useState(
|
||||
groupArray(
|
||||
db.relations?.from(route.params.item, "note") || [],
|
||||
db.settings?.getGroupOptions("notes")
|
||||
qclone(route?.params.item?.topics) || [],
|
||||
db.settings?.getGroupOptions("topics")
|
||||
)
|
||||
);
|
||||
const params = useRef<NotebookScreenParams>(route?.params);
|
||||
|
||||
useNavigationFocus(navigation, {
|
||||
onFocus: () => {
|
||||
Navigation.routeNeedsUpdate(route.name, onRequestUpdate);
|
||||
syncWithNavigation();
|
||||
useNavigationStore.getState().setButtonAction(openEditor);
|
||||
useNavigationStore.getState().setButtonAction(onPressFloatingButton);
|
||||
return false;
|
||||
},
|
||||
onBlur: () => {
|
||||
setOnFirstSave(null);
|
||||
return false;
|
||||
}
|
||||
onBlur: () => false
|
||||
});
|
||||
|
||||
const syncWithNavigation = React.useCallback(() => {
|
||||
@@ -68,10 +70,6 @@ const Notebook = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
||||
},
|
||||
params.current?.canGoBack
|
||||
);
|
||||
setOnFirstSave({
|
||||
type: "notebook",
|
||||
id: params.current.item.id
|
||||
});
|
||||
SearchService.prepareSearch = prepareSearch;
|
||||
}, [route.name]);
|
||||
|
||||
@@ -84,9 +82,11 @@ const Notebook = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
||||
?.data as NotebookType;
|
||||
if (notebook) {
|
||||
params.current.item = notebook;
|
||||
const notes = db.relations?.from(notebook, "note");
|
||||
setNotes(
|
||||
groupArray(notes || [], db.settings?.getGroupOptions("notes"))
|
||||
setTopics(
|
||||
groupArray(
|
||||
qclone(notebook.topics),
|
||||
db.settings?.getGroupOptions("topics")
|
||||
)
|
||||
);
|
||||
syncWithNavigation();
|
||||
}
|
||||
@@ -102,61 +102,60 @@ const Notebook = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOnNewTopicAdded, onRequestUpdate);
|
||||
};
|
||||
}, [onRequestUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setOnFirstSave(null);
|
||||
};
|
||||
}, []);
|
||||
}, [onRequestUpdate, topics]);
|
||||
|
||||
const prepareSearch = () => {
|
||||
SearchService.update({
|
||||
placeholder: `Search in "${params.current.title}"`,
|
||||
type: "notes",
|
||||
type: "topics",
|
||||
title: params.current.title,
|
||||
get: () => {
|
||||
const notebook = db.notebooks?.notebook(params?.current?.item?.id)
|
||||
?.data as NotebookType;
|
||||
return db.relations?.from(notebook, "note");
|
||||
return notebook?.topics;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onPressFloatingButton = () => {
|
||||
const n = params.current.item;
|
||||
eSendEvent(eOpenAddTopicDialog, { notebookId: n.id });
|
||||
};
|
||||
|
||||
const PLACEHOLDER_DATA = {
|
||||
heading: params.current.item?.title,
|
||||
paragraph: "You have not added any notes yet.",
|
||||
button: "Add your first note",
|
||||
action: openEditor,
|
||||
loading: "Loading notebook notes"
|
||||
paragraph: "You have not added any topics yet.",
|
||||
button: "Add first topic",
|
||||
action: onPressFloatingButton,
|
||||
loading: "Loading notebook topics"
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DelayLayout>
|
||||
<List
|
||||
listData={notes}
|
||||
type="notes"
|
||||
refreshCallback={() => {
|
||||
onRequestUpdate();
|
||||
}}
|
||||
screen="Notebook"
|
||||
headerProps={{
|
||||
heading: params.current.title
|
||||
}}
|
||||
loading={false}
|
||||
ListHeader={
|
||||
<NotebookHeader
|
||||
onEditNotebook={() => {
|
||||
eSendEvent(eOpenAddNotebookDialog, params.current.item);
|
||||
}}
|
||||
notebook={params.current.item}
|
||||
/>
|
||||
}
|
||||
placeholderData={PLACEHOLDER_DATA}
|
||||
/>
|
||||
</DelayLayout>
|
||||
</>
|
||||
<DelayLayout>
|
||||
<List
|
||||
listData={topics}
|
||||
type="topics"
|
||||
refreshCallback={() => {
|
||||
onRequestUpdate();
|
||||
}}
|
||||
screen="Notebook"
|
||||
headerProps={{
|
||||
heading: params.current.title
|
||||
}}
|
||||
loading={false}
|
||||
ListHeader={
|
||||
<NotebookHeader
|
||||
onEditNotebook={() => {
|
||||
eSendEvent(eOpenAddNotebookDialog, params.current.item);
|
||||
}}
|
||||
notebook={params.current.item}
|
||||
/>
|
||||
}
|
||||
placeholderData={PLACEHOLDER_DATA}
|
||||
/>
|
||||
|
||||
<FloatingButton title="Add new topic" onPress={onPressFloatingButton} />
|
||||
</DelayLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -18,12 +18,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Config } from "react-native-config";
|
||||
import { db } from "../../common/database";
|
||||
import { FloatingButton } from "../../components/container/floating-button";
|
||||
import DelayLayout from "../../components/delay-layout";
|
||||
import { AddNotebookEvent } from "../../components/dialog-provider/recievers";
|
||||
import List from "../../components/list";
|
||||
import { AddNotebookSheet } from "../../components/sheets/add-notebook";
|
||||
import { Walkthrough } from "../../components/walkthroughs";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import Navigation, { NavigationProps } from "../../services/navigation";
|
||||
@@ -31,9 +30,10 @@ import SearchService from "../../services/search";
|
||||
import SettingsService from "../../services/settings";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { useNotebookStore } from "../../stores/use-notebook-store";
|
||||
import { Config } from "react-native-config";
|
||||
|
||||
const onPressFloatingButton = () => {
|
||||
AddNotebookSheet.present();
|
||||
AddNotebookEvent();
|
||||
};
|
||||
|
||||
const prepareSearch = () => {
|
||||
@@ -83,7 +83,7 @@ export const Notebooks = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<DelayLayout delay={1}>
|
||||
<DelayLayout>
|
||||
<List
|
||||
listData={notebooks}
|
||||
type="notebooks"
|
||||
|
||||
@@ -24,7 +24,7 @@ import Navigation from "../../services/navigation";
|
||||
import { useMenuStore } from "../../stores/use-menu-store";
|
||||
import { NotesScreenParams } from "../../stores/use-navigation-store";
|
||||
import { useTagStore } from "../../stores/use-tag-store";
|
||||
import { eOnLoadNote, eOnTopicSheetUpdate } from "../../utils/events";
|
||||
import { eOnLoadNote } from "../../utils/events";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import { TopicType } from "../../utils/types";
|
||||
@@ -81,31 +81,12 @@ export const setOnFirstSave = (
|
||||
editorState().onNoteCreated = null;
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
editorState().onNoteCreated = (id) => onNoteCreated(id, data);
|
||||
}, 0);
|
||||
editorState().onNoteCreated = (id) => onNoteCreated(id, data);
|
||||
};
|
||||
|
||||
async function onNoteCreated(id: string, params: FirstSaveData) {
|
||||
if (!params) return;
|
||||
switch (params.type) {
|
||||
case "notebook": {
|
||||
await db.relations?.add(
|
||||
{ type: "notebook", id: params.id },
|
||||
{ type: "note", id: id }
|
||||
);
|
||||
Navigation.queueRoutesForUpdate(
|
||||
"TaggedNotes",
|
||||
"ColoredNotes",
|
||||
"TopicNotes",
|
||||
"Favorites",
|
||||
"Notes",
|
||||
"Notebook",
|
||||
"Notebooks"
|
||||
);
|
||||
editorState().onNoteCreated = null;
|
||||
break;
|
||||
}
|
||||
case "topic": {
|
||||
if (!params.notebook) break;
|
||||
await db.notes?.addToNotebook(
|
||||
@@ -125,7 +106,6 @@ async function onNoteCreated(id: string, params: FirstSaveData) {
|
||||
"Notebook",
|
||||
"Notebooks"
|
||||
);
|
||||
eSendEvent(eOnTopicSheetUpdate);
|
||||
break;
|
||||
}
|
||||
case "tag": {
|
||||
|
||||
@@ -42,13 +42,6 @@ import {
|
||||
setOnFirstSave,
|
||||
toCamelCase
|
||||
} from "./common";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import Notebook from "../notebook/index";
|
||||
export const WARNING_DATA = {
|
||||
title: "Some notes in this topic are not synced"
|
||||
};
|
||||
@@ -99,18 +92,13 @@ const NotesPage = ({
|
||||
}: RouteProps<
|
||||
"NotesPage" | "TaggedNotes" | "Monographs" | "ColoredNotes" | "TopicNotes"
|
||||
>) => {
|
||||
const colors = useThemeStore((state) => state.colors);
|
||||
const params = useRef<NotesScreenParams>(route?.params);
|
||||
const [notes, setNotes] = useState<NoteType[]>(get(route.params, true));
|
||||
const loading = useNoteStore((state) => state.loading);
|
||||
const [loadingNotes, setLoadingNotes] = useState(false);
|
||||
const alias = getAlias(params.current);
|
||||
const isMonograph = route.name === "Monographs";
|
||||
const notebook =
|
||||
route.name === "TopicNotes" && (params.current.item as TopicType).notebookId
|
||||
? db.notebooks?.notebook((params.current.item as TopicType).notebookId)
|
||||
?.data
|
||||
: null;
|
||||
|
||||
const isFocused = useNavigationFocus(navigation, {
|
||||
onFocus: (prev) => {
|
||||
Navigation.routeNeedsUpdate(route.name, onRequestUpdate);
|
||||
@@ -188,7 +176,6 @@ const NotesPage = ({
|
||||
) {
|
||||
return Navigation.goBack();
|
||||
}
|
||||
if (notes.length === 0) setLoadingNotes(false);
|
||||
setNotes(notes);
|
||||
syncWithNavigation();
|
||||
} catch (e) {
|
||||
@@ -200,7 +187,7 @@ const NotesPage = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingNotes) {
|
||||
setTimeout(() => setLoadingNotes(false), 50);
|
||||
setTimeout(() => setLoadingNotes(false), 300);
|
||||
}
|
||||
}, [loadingNotes, notes]);
|
||||
|
||||
@@ -221,45 +208,6 @@ const NotesPage = ({
|
||||
}
|
||||
wait={loading || loadingNotes}
|
||||
>
|
||||
{route.name === "TopicNotes" ? (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
paddingHorizontal: 12,
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
// borderBottomWidth: 1,
|
||||
// borderBottomColor: colors.nav
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
onPress={() => {
|
||||
Navigation.navigate(
|
||||
{
|
||||
name: "Notebooks"
|
||||
},
|
||||
{}
|
||||
);
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
>
|
||||
Notebooks
|
||||
</Paragraph>
|
||||
<IconButton
|
||||
name="chevron-right"
|
||||
size={14}
|
||||
customStyle={{ width: 25, height: 25 }}
|
||||
/>
|
||||
<Paragraph
|
||||
onPress={() => {
|
||||
Notebook.navigate(notebook, true);
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
>
|
||||
{notebook.title}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
<List
|
||||
listData={notes}
|
||||
type="notes"
|
||||
|
||||
@@ -58,18 +58,16 @@ export const TopicNotes = ({
|
||||
route
|
||||
}: NavigationProps<"TopicNotes">) => {
|
||||
return (
|
||||
<>
|
||||
<NotesPage
|
||||
navigation={navigation}
|
||||
route={route}
|
||||
get={TopicNotes.get}
|
||||
placeholderData={PLACEHOLDER_DATA}
|
||||
onPressFloatingButton={openEditor}
|
||||
rightButtons={headerRightButtons}
|
||||
canGoBack={route.params.canGoBack}
|
||||
focusControl={true}
|
||||
/>
|
||||
</>
|
||||
<NotesPage
|
||||
navigation={navigation}
|
||||
route={route}
|
||||
get={TopicNotes.get}
|
||||
placeholderData={PLACEHOLDER_DATA}
|
||||
onPressFloatingButton={openEditor}
|
||||
rightButtons={headerRightButtons}
|
||||
canGoBack={route.params.canGoBack}
|
||||
focusControl={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ 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 EventManager from "@notesnook/core/utils/event-manager";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import EventManager from "@notesnook/core/utils/event-manager";
|
||||
import { RefObject } from "react";
|
||||
import { ActionSheetRef } from "react-native-actions-sheet";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import Config from "react-native-config";
|
||||
import {
|
||||
eCloseSheet,
|
||||
@@ -97,7 +97,7 @@ export type PresentSheetOptions = {
|
||||
component:
|
||||
| JSX.Element
|
||||
| ((
|
||||
ref: RefObject<ActionSheetRef>,
|
||||
ref: RefObject<ActionSheet>,
|
||||
close?: (ctx?: string) => void,
|
||||
update?: (props: PresentSheetOptions) => void
|
||||
) => JSX.Element);
|
||||
@@ -114,7 +114,6 @@ export type PresentSheetOptions = {
|
||||
actionsArray: SheetAction[];
|
||||
learnMore: string;
|
||||
learnMorePress: () => void;
|
||||
enableGesturesInScrollView: boolean;
|
||||
};
|
||||
|
||||
export function presentSheet(data: Partial<PresentSheetOptions>) {
|
||||
|
||||
@@ -145,23 +145,23 @@ export const useTip = (
|
||||
|
||||
const tips: TTip[] = [
|
||||
{
|
||||
text: "You can swipe left anywhere in the app to start a new note.",
|
||||
text: "You can swipe left anywhere in the app to start a new note",
|
||||
contexts: ["notes", "first-note"]
|
||||
},
|
||||
{
|
||||
text: "Long press on any item in list to enter multi-select mode.",
|
||||
text: "Long press on any item in list to open quick actions menu.",
|
||||
contexts: ["notes", "notebook", "notebook", "tags", "topics"]
|
||||
},
|
||||
{
|
||||
text: "Monographs enable you to share your notes in a secure and private way.",
|
||||
text: "Monographs enable you to share your notes in a secure and private way",
|
||||
contexts: ["monographs"]
|
||||
},
|
||||
{
|
||||
text: "Monographs can be encrypted with a secret key and shared with anyone.",
|
||||
text: "Monographs can be encrypted with a secret key and shared with anyone",
|
||||
contexts: ["monographs"]
|
||||
},
|
||||
{
|
||||
text: "You can pin frequently used Notebooks to the Side Menu to quickly access them.",
|
||||
text: "Frequently accessed notebooks can be pinned to Side Menu so that they are easily accessible",
|
||||
contexts: ["notebook", "notebooks"]
|
||||
},
|
||||
{
|
||||
@@ -173,15 +173,15 @@ const tips: TTip[] = [
|
||||
contexts: ["notebook", "topics"]
|
||||
},
|
||||
{
|
||||
text: "Mark important notes by adding them to favorites.",
|
||||
text: "Items in trash are kept for 7 days after which they are permanently deleted.",
|
||||
contexts: ["trash"]
|
||||
},
|
||||
{
|
||||
text: "Mark important notes by adding them to favorites",
|
||||
contexts: ["notes"]
|
||||
},
|
||||
{
|
||||
text: "Are you scrolling a lot to find a specific note? Pin it to the top from Note properties.",
|
||||
contexts: ["notes"]
|
||||
},
|
||||
{
|
||||
text: "You can view & restore older versions of any note by going to its properties -> History.",
|
||||
text: "Have to scroll down a lot to open a note you are working on? Pin it to top from properties.",
|
||||
contexts: ["notes"]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -158,5 +158,3 @@ export const eCloseAnnouncementDialog = "604";
|
||||
|
||||
export const eOpenLoading = "605";
|
||||
export const eCloseLoading = "606";
|
||||
|
||||
export const eOnTopicSheetUpdate = "607";
|
||||
|
||||
@@ -29,28 +29,22 @@ import { eClearEditor } from "./events";
|
||||
import { useRelationStore } from "../stores/use-relation-store";
|
||||
import { presentDialog } from "../components/dialog/functions";
|
||||
|
||||
function deleteConfirmDialog(items, type, context) {
|
||||
function deleteNotesConfirmDialog(items, type, context) {
|
||||
return new Promise((resolve) => {
|
||||
presentDialog({
|
||||
title: `Delete ${
|
||||
items.length > 1 ? `${items.length} ${type}s` : `${type}`
|
||||
title: "Delete Contained Notes?",
|
||||
paragraph: `Do you want to delete notes within ${
|
||||
items.length > 1 ? `these ${type}s` : `this ${type}`
|
||||
}?`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
positivePress: (value) => {
|
||||
console.log(value);
|
||||
resolve({ delete: true, deleteNotes: value });
|
||||
positiveText: "Yes",
|
||||
negativeText: "No",
|
||||
positivePress: () => {
|
||||
resolve(true);
|
||||
},
|
||||
onClose: () => {
|
||||
resolve({ delete: false });
|
||||
resolve(false);
|
||||
},
|
||||
context: context,
|
||||
check: {
|
||||
info: `Move all notes in ${
|
||||
items.length > 1 ? `these ${type}s` : `this ${type}`
|
||||
} to trash`,
|
||||
type: "transparent"
|
||||
}
|
||||
context: context
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -113,49 +107,50 @@ export const deleteItems = async (item, context) => {
|
||||
}
|
||||
|
||||
if (topics?.length > 0) {
|
||||
const result = await deleteConfirmDialog(topics, "topic", context);
|
||||
if (result.delete) {
|
||||
for (const topic of topics) {
|
||||
if (result.deleteNotes) {
|
||||
const deleteNotes = await deleteNotesConfirmDialog(
|
||||
topics,
|
||||
"topic",
|
||||
context
|
||||
);
|
||||
for (const topic of topics) {
|
||||
if (deleteNotes) {
|
||||
const notes = db.notebooks
|
||||
.notebook(topic.notebookId)
|
||||
.topics.topic(topic.id).all;
|
||||
await db.notes.delete(...notes.map((note) => note.id));
|
||||
}
|
||||
await db.notebooks.notebook(topic.notebookId).topics.delete(topic.id);
|
||||
}
|
||||
routesForUpdate.push("Notebook", "Notebooks");
|
||||
useMenuStore.getState().setMenuPins();
|
||||
ToastEvent.show({
|
||||
heading: `${topics.length > 1 ? "Topics" : "Topic"} deleted`,
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
|
||||
if (notebooks?.length > 0) {
|
||||
const deleteNotes = await deleteNotesConfirmDialog(
|
||||
notebooks,
|
||||
"notebook",
|
||||
context
|
||||
);
|
||||
|
||||
let ids = notebooks.map((i) => i.id);
|
||||
if (deleteNotes) {
|
||||
for (let id of ids) {
|
||||
const topics = db.notebooks.notebook(id).topics.all;
|
||||
for (let topic of topics) {
|
||||
const notes = db.notebooks
|
||||
.notebook(topic.notebookId)
|
||||
.topics.topic(topic.id).all;
|
||||
await db.notes.delete(...notes.map((note) => note.id));
|
||||
}
|
||||
await db.notebooks.notebook(topic.notebookId).topics.delete(topic.id);
|
||||
}
|
||||
routesForUpdate.push("Notebook", "Notebooks");
|
||||
useMenuStore.getState().setMenuPins();
|
||||
ToastEvent.show({
|
||||
heading: `${topics.length > 1 ? "Topics" : "Topic"} deleted`,
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (notebooks?.length > 0) {
|
||||
const result = await deleteConfirmDialog(notebooks, "notebook", context);
|
||||
|
||||
if (result.delete) {
|
||||
let ids = notebooks.map((i) => i.id);
|
||||
if (result.deleteNotes) {
|
||||
for (let id of ids) {
|
||||
const notebook = db.notebooks.notebook(id);
|
||||
const topics = notebook.topics.all;
|
||||
for (let topic of topics) {
|
||||
const notes = db.notebooks
|
||||
.notebook(topic.notebookId)
|
||||
.topics.topic(topic.id).all;
|
||||
await db.notes.delete(...notes.map((note) => note.id));
|
||||
}
|
||||
const notes = db.relations.from(notebook.data, "note");
|
||||
await db.notes.delete(...notes.map((note) => note.id));
|
||||
}
|
||||
}
|
||||
await db.notebooks.delete(...ids);
|
||||
routesForUpdate.push("Notebook", "Notebooks");
|
||||
useMenuStore.getState().setMenuPins();
|
||||
}
|
||||
await db.notebooks.delete(...ids);
|
||||
routesForUpdate.push("Notebook", "Notebooks");
|
||||
useMenuStore.getState().setMenuPins();
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate(...routesForUpdate);
|
||||
|
||||
@@ -29,8 +29,7 @@ import {
|
||||
navigate,
|
||||
elementByText,
|
||||
sleep,
|
||||
notVisibleByText,
|
||||
visibleById
|
||||
notVisibleByText
|
||||
} from "./utils";
|
||||
|
||||
async function createNotebook(
|
||||
@@ -129,11 +128,11 @@ describe("NOTEBOOKS", () => {
|
||||
await device.pressBack();
|
||||
await sleep(500);
|
||||
await tapByText("Notebook 1");
|
||||
await tapById("add-topic-button");
|
||||
await tapById(notesnook.buttons.add);
|
||||
await elementById("input-title").typeText("Topic");
|
||||
await tapByText("Add");
|
||||
await sleep(500);
|
||||
await visibleById("topic-sheet-item-0");
|
||||
await visibleByText("Topic");
|
||||
});
|
||||
|
||||
it("Edit topic", async () => {
|
||||
@@ -146,11 +145,12 @@ describe("NOTEBOOKS", () => {
|
||||
await sleep(500);
|
||||
await tapByText("Notebook 1");
|
||||
await sleep(300);
|
||||
await visibleById("topic-sheet-item-0");
|
||||
await visibleByText("Topic");
|
||||
await tapById(notesnook.ids.notebook.menu);
|
||||
await tapByText("Edit topic");
|
||||
await elementById("input-title").typeText(" (edited)");
|
||||
await tapByText("Save");
|
||||
await visibleByText("Topic (edited)");
|
||||
});
|
||||
|
||||
it("Add new note to topic", async () => {
|
||||
@@ -175,11 +175,12 @@ describe("NOTEBOOKS", () => {
|
||||
await tapByText("Topic");
|
||||
let note = await createNote();
|
||||
await elementByText(note.body).longPress();
|
||||
await tapByText("Select");
|
||||
await tapById("select-minus");
|
||||
await notVisibleById(note.title);
|
||||
});
|
||||
|
||||
it("Add/Remove note to notebook from home", async () => {
|
||||
it.only("Add/Remove note to notebook from home", async () => {
|
||||
await prepare();
|
||||
await navigate("Notebooks");
|
||||
await sleep(500);
|
||||
@@ -191,31 +192,31 @@ describe("NOTEBOOKS", () => {
|
||||
await createNote();
|
||||
console.log("ADD TO A SINGLE TOPIC");
|
||||
await tapById(notesnook.listitem.menu);
|
||||
await tapById("icon-notebooks");
|
||||
await tapById("icon-Add to notebook");
|
||||
await sleep(500);
|
||||
await tapByText("Notebook 1");
|
||||
await tapByText("Topic");
|
||||
await tapByText("Save");
|
||||
await sleep(300);
|
||||
await visibleByText("Topic");
|
||||
await visibleByText("Notebook 1 › Topic");
|
||||
console.log("MOVE FROM ONE TOPIC TO ANOTHER");
|
||||
await tapById(notesnook.listitem.menu);
|
||||
await tapById("icon-notebooks");
|
||||
await tapById("icon-Add to notebook");
|
||||
await tapByText("Notebook 1");
|
||||
await tapByText("Topic 2");
|
||||
await tapByText("Save");
|
||||
await visibleByText("Topic 2");
|
||||
await visibleByText("Notebook 1 › Topic 2");
|
||||
console.log("REMOVE FROM TOPIC");
|
||||
await tapById(notesnook.listitem.menu);
|
||||
await tapById("icon-notebooks");
|
||||
await tapById("icon-Add to notebook");
|
||||
await tapByText("Notebook 1");
|
||||
await tapByText("Topic 2");
|
||||
await tapByText("Save");
|
||||
await sleep(300);
|
||||
await notVisibleByText("Topic 2");
|
||||
await notVisibleByText("Notebook 1 › Topic 2");
|
||||
console.log("MOVE TO MULTIPLE TOPICS");
|
||||
await tapById(notesnook.listitem.menu);
|
||||
await tapById("icon-notebooks");
|
||||
await tapById("icon-Add to notebook");
|
||||
await tapByText("Notebook 1");
|
||||
await elementByText("Topic").longPress();
|
||||
await visibleByText("Reset selection");
|
||||
|
||||
27
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "2.4.6",
|
||||
"version": "2.4.5",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "2.4.6",
|
||||
"version": "2.4.5",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
"native/",
|
||||
@@ -40,9 +40,7 @@
|
||||
"html-to-text": "8.1.0",
|
||||
"phone": "^3.1.14",
|
||||
"qclone": "^1.2.0",
|
||||
"react": "18.0.0",
|
||||
"react-native": "0.69.7",
|
||||
"react-native-actions-sheet": "^0.9.0-alpha.11",
|
||||
"react-native-actions-sheet": "^0.7.2",
|
||||
"react-native-check-version": "https://github.com/flexible-agency/react-native-check-version",
|
||||
"react-native-drax": "^0.10.2",
|
||||
"react-native-image-zoom-viewer": "^3.0.1",
|
||||
@@ -17957,12 +17955,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-actions-sheet": {
|
||||
"version": "0.9.0-alpha.11",
|
||||
"resolved": "https://registry.npmjs.org/react-native-actions-sheet/-/react-native-actions-sheet-0.9.0-alpha.11.tgz",
|
||||
"integrity": "sha512-bYOQVpB3lHcBh+wYa+6IEXfQC3xrlDJh+yhdIhVMjSRybvgcHGSIali8cPrGuwRF7luF0fqZIS+xsEDQs5eahA==",
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-actions-sheet/-/react-native-actions-sheet-0.7.2.tgz",
|
||||
"integrity": "sha512-au9QkDnSC+lhiTMHYA2cNdOhrKW/6v/vdeOTNigRFuvYoVVS2+vJOJpt2Z3mumRjmD02UocV0WmHu4anSsqqpA==",
|
||||
"peerDependencies": {
|
||||
"react-native": "*",
|
||||
"react-native-gesture-handler": "*"
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-actions-shortcuts": {
|
||||
@@ -24310,9 +24307,7 @@
|
||||
"html-to-text": "8.1.0",
|
||||
"phone": "^3.1.14",
|
||||
"qclone": "^1.2.0",
|
||||
"react": "18.0.0",
|
||||
"react-native": "0.69.7",
|
||||
"react-native-actions-sheet": "^0.9.0-alpha.11",
|
||||
"react-native-actions-sheet": "^0.7.2",
|
||||
"react-native-check-version": "https://github.com/flexible-agency/react-native-check-version",
|
||||
"react-native-drax": "^0.10.2",
|
||||
"react-native-image-zoom-viewer": "^3.0.1",
|
||||
@@ -34856,9 +34851,9 @@
|
||||
}
|
||||
},
|
||||
"react-native-actions-sheet": {
|
||||
"version": "0.9.0-alpha.11",
|
||||
"resolved": "https://registry.npmjs.org/react-native-actions-sheet/-/react-native-actions-sheet-0.9.0-alpha.11.tgz",
|
||||
"integrity": "sha512-bYOQVpB3lHcBh+wYa+6IEXfQC3xrlDJh+yhdIhVMjSRybvgcHGSIali8cPrGuwRF7luF0fqZIS+xsEDQs5eahA=="
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-actions-sheet/-/react-native-actions-sheet-0.7.2.tgz",
|
||||
"integrity": "sha512-au9QkDnSC+lhiTMHYA2cNdOhrKW/6v/vdeOTNigRFuvYoVVS2+vJOJpt2Z3mumRjmD02UocV0WmHu4anSsqqpA=="
|
||||
},
|
||||
"react-native-actions-shortcuts": {
|
||||
"version": "1.0.1",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"repack": "cd native && react-native webpack-start",
|
||||
"install-pods": "cd native/ios && pod install",
|
||||
"build-ios": "cd native && detox build -c ios.sim.release",
|
||||
"build-android": " cd native && detox build -c android.emu.release",
|
||||
"build-android": "cd native && detox build -c android.emu.release",
|
||||
"e2e-android": "cd native && detox test --configuration android.emu.release --detectOpenHandles",
|
||||
"e2e-ios": "cd native && detox test -c ios.sim.release --detectOpenHandles",
|
||||
"bump": "cd native && npx react-native bump-version --skip-semver-for android",
|
||||
@@ -36,4 +36,4 @@
|
||||
"react-native-fingerprint-scanner": "https://github.com/ammarahm-ed/react-native-fingerprint-scanner.git",
|
||||
"react-native-iap": "7.5.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,16 @@
|
||||
diff --git a/node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js b/node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js
|
||||
index 4536402..2a100d9 100644
|
||||
index 4536402..5ceaf65 100644
|
||||
--- a/node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js
|
||||
+++ b/node_modules/react-native/Libraries/Components/Keyboard/KeyboardAvoidingView.js
|
||||
@@ -64,7 +64,7 @@ class KeyboardAvoidingView extends React.Component<Props, State> {
|
||||
_subscriptions: Array<EventSubscription> = [];
|
||||
viewRef: {current: React.ElementRef<typeof View> | null, ...};
|
||||
_initialFrameHeight: number = 0;
|
||||
-
|
||||
+ keyboardShown = false;
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {bottom: 0};
|
||||
@@ -80,7 +80,9 @@ class KeyboardAvoidingView extends React.Component<Props, State> {
|
||||
const keyboardY =
|
||||
keyboardFrame.screenY - (this.props.keyboardVerticalOffset ?? 0);
|
||||
@@ -82,6 +82,7 @@ class KeyboardAvoidingView extends React.Component<Props, State> {
|
||||
|
||||
- // Calculate the displacement needed for the view such that it
|
||||
+
|
||||
+ if (this._initialFrameHeight && frame.height < this._initialFrameHeight && this.keyboardShown) frame.height = this._initialFrameHeight;
|
||||
+ // Calculate the displacement needed for the view such that it
|
||||
// Calculate the displacement needed for the view such that it
|
||||
// no longer overlaps with the keyboard
|
||||
+ if (this._initialFrameHeight && frame.height < this._initialFrameHeight) frame.height = this._initialFrameHeight;
|
||||
return Math.max(frame.y + frame.height - keyboardY, 0);
|
||||
}
|
||||
@@ -92,7 +94,9 @@ class KeyboardAvoidingView extends React.Component<Props, State> {
|
||||
|
||||
@@ -92,7 +93,9 @@ class KeyboardAvoidingView extends React.Component<Props, State> {
|
||||
|
||||
_onLayout = (event: ViewLayoutEvent) => {
|
||||
const wasFrameNull = this._frame == null;
|
||||
@@ -32,19 +20,6 @@ index 4536402..2a100d9 100644
|
||||
if (!this._initialFrameHeight) {
|
||||
// save the initial frame height, before the keyboard is visible
|
||||
this._initialFrameHeight = this._frame.height;
|
||||
@@ -142,6 +146,8 @@ class KeyboardAvoidingView extends React.Component<Props, State> {
|
||||
this._subscriptions = [
|
||||
Keyboard.addListener('keyboardDidHide', this._onKeyboardChange),
|
||||
Keyboard.addListener('keyboardDidShow', this._onKeyboardChange),
|
||||
+ Keyboard.addListener('keyboardDidHide', () => this.keyboardShown = true),
|
||||
+ Keyboard.addListener('keyboardDidShow', () => this.keyboardShown = false),
|
||||
];
|
||||
}
|
||||
}
|
||||
diff --git a/node_modules/react-native/React/.DS_Store b/node_modules/react-native/React/.DS_Store
|
||||
new file mode 100644
|
||||
index 0000000..5155fbe
|
||||
Binary files /dev/null and b/node_modules/react-native/React/.DS_Store differ
|
||||
diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
|
||||
index f0f6402..d645d81 100644
|
||||
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
|
||||
@@ -69,13 +44,6 @@ index f0f6402..d645d81 100644
|
||||
CGFloat smallerOffset = 0.0;
|
||||
CGFloat largerOffset = maximumOffset;
|
||||
|
||||
diff --git a/node_modules/react-native/scripts/.packager.env b/node_modules/react-native/scripts/.packager.env
|
||||
new file mode 100644
|
||||
index 0000000..361f5fb
|
||||
--- /dev/null
|
||||
+++ b/node_modules/react-native/scripts/.packager.env
|
||||
@@ -0,0 +1 @@
|
||||
+export RCT_METRO_PORT=8081
|
||||
diff --git a/node_modules/react-native/scripts/packager.sh b/node_modules/react-native/scripts/packager.sh
|
||||
index b9f9016..8859130 100755
|
||||
--- a/node_modules/react-native/scripts/packager.sh
|
||||
|
||||
@@ -132,7 +132,7 @@ export class AppModel {
|
||||
.waitFor({ state: "visible" });
|
||||
}
|
||||
|
||||
async search(query: string, type: string) {
|
||||
async search(query: string) {
|
||||
const searchinput = this.page.locator(getTestId("search-input"));
|
||||
const searchButton = this.page.locator(getTestId("search-button"));
|
||||
const openSearch = this.page.locator(getTestId("open-search"));
|
||||
@@ -140,6 +140,6 @@ export class AppModel {
|
||||
await openSearch.click();
|
||||
await searchinput.fill(query);
|
||||
await searchButton.click();
|
||||
return new SearchViewModel(this.page, type);
|
||||
return new SearchViewModel(this.page);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,17 +29,14 @@ export class BaseViewModel {
|
||||
private readonly listPlaceholder: Locator;
|
||||
private readonly sortByButton: Locator;
|
||||
|
||||
constructor(page: Page, pageId: string, listType: string) {
|
||||
constructor(page: Page, pageId: string) {
|
||||
this.page = page;
|
||||
this.list = page.locator(`#${pageId} >> ${getTestId(`${listType}-list`)}`);
|
||||
this.list = page.locator(`#${pageId} >> ${getTestId("note-list")}`);
|
||||
this.listPlaceholder = page.locator(
|
||||
`#${pageId} >> ${getTestId("list-placeholder")}`
|
||||
);
|
||||
|
||||
this.sortByButton = this.page.locator(
|
||||
// TODO:
|
||||
getTestId(`${pageId === "notebook" ? "notes" : pageId}-sort-button`)
|
||||
);
|
||||
this.sortByButton = this.list.locator(getTestId("sort-icon-button"));
|
||||
}
|
||||
|
||||
async findGroup(groupName: string) {
|
||||
@@ -106,15 +103,13 @@ export class BaseViewModel {
|
||||
async sort(sort: SortOptions) {
|
||||
const contextMenu: ContextMenuModel = new ContextMenuModel(this.page);
|
||||
|
||||
if (sort.groupBy) {
|
||||
await contextMenu.open(this.sortByButton, "left");
|
||||
await contextMenu.clickOnItem("groupBy");
|
||||
if (!(await contextMenu.hasItem(sort.groupBy))) {
|
||||
await contextMenu.close();
|
||||
return false;
|
||||
}
|
||||
await contextMenu.clickOnItem(sort.groupBy);
|
||||
await contextMenu.open(this.sortByButton, "left");
|
||||
await contextMenu.clickOnItem("groupBy");
|
||||
if (!(await contextMenu.hasItem(sort.groupBy))) {
|
||||
await contextMenu.close();
|
||||
return false;
|
||||
}
|
||||
await contextMenu.clickOnItem(sort.groupBy);
|
||||
|
||||
await contextMenu.open(this.sortByButton, "left");
|
||||
await contextMenu.clickOnItem("sortDirection");
|
||||
@@ -136,10 +131,7 @@ export class BaseViewModel {
|
||||
}
|
||||
|
||||
async isEmpty() {
|
||||
const items = this.list.locator(
|
||||
`${getTestId(`virtuoso-item-list`)} >> ${getTestId("list-item")}`
|
||||
);
|
||||
const totalItems = await items.count();
|
||||
const totalItems = await this.list.locator(getTestId("list-item")).count();
|
||||
return totalItems <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,21 +22,18 @@ import { BaseItemModel } from "./base-item.model";
|
||||
import { ContextMenuModel } from "./context-menu.model";
|
||||
import { NotesViewModel } from "./notes-view.model";
|
||||
import { Item } from "./types";
|
||||
import { confirmDialog, fillItemDialog } from "./utils";
|
||||
import { confirmDialog, denyDialog, fillItemDialog } from "./utils";
|
||||
|
||||
export class ItemModel extends BaseItemModel {
|
||||
private readonly contextMenu: ContextMenuModel;
|
||||
constructor(locator: Locator, private readonly id: "topic" | "tag") {
|
||||
constructor(locator: Locator) {
|
||||
super(locator);
|
||||
this.contextMenu = new ContextMenuModel(this.page);
|
||||
}
|
||||
|
||||
async open() {
|
||||
await this.locator.click();
|
||||
return new NotesViewModel(
|
||||
this.page,
|
||||
this.id === "topic" ? "notebook" : "notes"
|
||||
);
|
||||
return new NotesViewModel(this.page, "notes");
|
||||
}
|
||||
|
||||
async delete() {
|
||||
@@ -50,10 +47,9 @@ export class ItemModel extends BaseItemModel {
|
||||
await this.contextMenu.open(this.locator);
|
||||
await this.contextMenu.clickOnItem("delete");
|
||||
|
||||
if (deleteContainedNotes)
|
||||
await this.page.locator("#deleteContainingNotes").check({ force: true });
|
||||
if (deleteContainedNotes) await confirmDialog(this.page);
|
||||
else await denyDialog(this.page);
|
||||
|
||||
await confirmDialog(this.page);
|
||||
await this.waitFor("detached");
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export class ItemsViewModel extends BaseViewModel {
|
||||
private readonly createButton: Locator;
|
||||
|
||||
constructor(page: Page, private readonly id: "topics" | "tags") {
|
||||
super(page, id, id);
|
||||
super(page, id);
|
||||
this.createButton = page.locator(getTestId(`${id}-action-button`));
|
||||
}
|
||||
|
||||
@@ -45,11 +45,7 @@ export class ItemsViewModel extends BaseViewModel {
|
||||
async findItem(item: Item) {
|
||||
const titleToCompare = this.id === "tags" ? `#${item.title}` : item.title;
|
||||
for await (const _item of this.iterateItems()) {
|
||||
const itemModel = new ItemModel(
|
||||
_item,
|
||||
// TODO:
|
||||
this.id === "topics" ? "topic" : "tag"
|
||||
);
|
||||
const itemModel = new ItemModel(_item);
|
||||
const title = await itemModel.getTitle();
|
||||
if (title === titleToCompare) return itemModel;
|
||||
}
|
||||
|
||||
@@ -23,8 +23,7 @@ import { ContextMenuModel } from "./context-menu.model";
|
||||
import { ToggleModel } from "./toggle.model";
|
||||
import { ItemsViewModel } from "./items-view.model";
|
||||
import { Notebook } from "./types";
|
||||
import { confirmDialog, fillNotebookDialog } from "./utils";
|
||||
import { NotesViewModel } from "./notes-view.model";
|
||||
import { confirmDialog, denyDialog, fillNotebookDialog } from "./utils";
|
||||
|
||||
export class NotebookItemModel extends BaseItemModel {
|
||||
private readonly contextMenu: ContextMenuModel;
|
||||
@@ -35,10 +34,7 @@ export class NotebookItemModel extends BaseItemModel {
|
||||
|
||||
async openNotebook() {
|
||||
await this.locator.click();
|
||||
return {
|
||||
topics: new ItemsViewModel(this.page, "topics"),
|
||||
notes: new NotesViewModel(this.page, "notebook")
|
||||
};
|
||||
return new ItemsViewModel(this.page, "topics");
|
||||
}
|
||||
|
||||
async editNotebook(notebook: Notebook) {
|
||||
@@ -52,10 +48,9 @@ export class NotebookItemModel extends BaseItemModel {
|
||||
await this.contextMenu.open(this.locator);
|
||||
await this.contextMenu.clickOnItem("movetotrash");
|
||||
|
||||
if (deleteContainedNotes)
|
||||
await this.page.locator("#deleteContainingNotes").check({ force: true });
|
||||
if (deleteContainedNotes) await confirmDialog(this.page);
|
||||
else await denyDialog(this.page);
|
||||
|
||||
await confirmDialog(this.page);
|
||||
await this.waitFor("detached");
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export class NotebooksViewModel extends BaseViewModel {
|
||||
private readonly createButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page, "notebooks", "notebooks");
|
||||
super(page, "notebooks");
|
||||
this.createButton = page
|
||||
.locator(getTestId("notebooks-action-button"))
|
||||
.first();
|
||||
|
||||
@@ -32,12 +32,9 @@ export class NotesViewModel extends BaseViewModel {
|
||||
private readonly createButton: Locator;
|
||||
readonly editor: EditorModel;
|
||||
|
||||
constructor(page: Page, pageId: "home" | "notes" | "notebook") {
|
||||
super(page, pageId, pageId === "home" ? "home" : "notes");
|
||||
this.createButton = page.locator(
|
||||
// TODO:
|
||||
getTestId(`${pageId === "notebook" ? "notebook" : "notes"}-action-button`)
|
||||
);
|
||||
constructor(page: Page, pageId: "home" | "notes") {
|
||||
super(page, pageId);
|
||||
this.createButton = page.locator(getTestId("notes-action-button"));
|
||||
this.editor = new EditorModel(page);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export class RemindersViewModel extends BaseViewModel {
|
||||
private readonly createButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page, "reminders", "reminders");
|
||||
super(page, "reminders");
|
||||
this.createButton = page
|
||||
.locator(getTestId("reminders-action-button"))
|
||||
.first();
|
||||
|
||||
@@ -23,8 +23,8 @@ import { ItemModel } from "./item.model";
|
||||
import { Item } from "./types";
|
||||
|
||||
export class SearchViewModel extends BaseViewModel {
|
||||
constructor(page: Page, type: string) {
|
||||
super(page, "general", type);
|
||||
constructor(page: Page) {
|
||||
super(page, "general");
|
||||
}
|
||||
|
||||
async findItem(item: Item) {
|
||||
|
||||
@@ -24,7 +24,7 @@ import { TrashItemModel } from "./trash-item.model";
|
||||
|
||||
export class TrashViewModel extends BaseViewModel {
|
||||
constructor(page: Page) {
|
||||
super(page, "trash", "trash");
|
||||
super(page, "trash");
|
||||
}
|
||||
|
||||
async findItem(title: string) {
|
||||
|
||||
@@ -45,7 +45,7 @@ export type GroupByOptions =
|
||||
| "week";
|
||||
|
||||
export type SortOptions = {
|
||||
groupBy?: GroupByOptions;
|
||||
groupBy: GroupByOptions;
|
||||
sortBy: SortByOptions;
|
||||
orderBy: OrderByOptions;
|
||||
};
|
||||
|
||||
@@ -43,19 +43,7 @@ test("create a note inside a notebook", async ({ page }) => {
|
||||
await app.goto();
|
||||
const notebooks = await app.goToNotebooks();
|
||||
const notebook = await notebooks.createNotebook(NOTEBOOK);
|
||||
const { notes } = (await notebook?.openNotebook()) || {};
|
||||
|
||||
const note = await notes?.createNote(NOTE);
|
||||
|
||||
expect(note).toBeDefined();
|
||||
});
|
||||
|
||||
test("create a note inside a topic", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notebooks = await app.goToNotebooks();
|
||||
const notebook = await notebooks.createNotebook(NOTEBOOK);
|
||||
const { topics } = (await notebook?.openNotebook()) || {};
|
||||
const topics = await notebook?.openNotebook();
|
||||
const topic = await topics?.findItem({ title: NOTEBOOK.topics[0] });
|
||||
const notes = await topic?.open();
|
||||
|
||||
@@ -182,27 +170,7 @@ test("delete all notes within a notebook", async ({ page }) => {
|
||||
await app.goto();
|
||||
const notebooks = await app.goToNotebooks();
|
||||
const notebook = await notebooks.createNotebook(NOTEBOOK);
|
||||
let { notes } = (await notebook?.openNotebook()) || {};
|
||||
for (let i = 0; i < 2; ++i) {
|
||||
await notes?.createNote({
|
||||
title: `Note ${i}`,
|
||||
content: NOTE.content
|
||||
});
|
||||
}
|
||||
await app.goBack();
|
||||
|
||||
await notebook?.moveToTrash(true);
|
||||
|
||||
notes = await app.goToNotes();
|
||||
expect(await notes.isEmpty()).toBe(true);
|
||||
});
|
||||
|
||||
test("delete all notes within a topic", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notebooks = await app.goToNotebooks();
|
||||
const notebook = await notebooks.createNotebook(NOTEBOOK);
|
||||
const { topics } = (await notebook?.openNotebook()) || {};
|
||||
const topics = await notebook?.openNotebook();
|
||||
const topic = await topics?.findItem({ title: NOTEBOOK.topics[0] });
|
||||
let notes = await topic?.open();
|
||||
for (let i = 0; i < 2; ++i) {
|
||||
|
||||
@@ -96,7 +96,9 @@ test("add a note to notebook", async ({ page }) => {
|
||||
});
|
||||
|
||||
expect(
|
||||
await app.toasts.waitForToast("1 note added to Hello and 3 others.")
|
||||
await app.toasts.waitForToast(
|
||||
"1 note added to 4 topics & removed from 0 topics."
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -20,14 +20,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { AppModel } from "./models/app.model";
|
||||
import { Item } from "./models/types";
|
||||
import { NOTEBOOK, sortByOptions, orderByOptions, NOTE } from "./utils";
|
||||
import {
|
||||
groupByOptions,
|
||||
NOTEBOOK,
|
||||
sortByOptions,
|
||||
orderByOptions,
|
||||
NOTE
|
||||
} from "./utils";
|
||||
|
||||
test("create shortcut of a topic", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notebooks = await app.goToNotebooks();
|
||||
const notebook = await notebooks.createNotebook(NOTEBOOK);
|
||||
const { topics } = (await notebook?.openNotebook()) || {};
|
||||
const topics = await notebook?.openNotebook();
|
||||
const topic = await topics?.findItem({ title: NOTEBOOK.topics[0] });
|
||||
|
||||
await topic?.createShortcut();
|
||||
@@ -42,7 +48,7 @@ test("remove shortcut of a topic", async ({ page }) => {
|
||||
await app.goto();
|
||||
const notebooks = await app.goToNotebooks();
|
||||
const notebook = await notebooks.createNotebook(NOTEBOOK);
|
||||
const { topics } = (await notebook?.openNotebook()) || {};
|
||||
const topics = await notebook?.openNotebook();
|
||||
const topic = await topics?.findItem({ title: NOTEBOOK.topics[0] });
|
||||
await topic?.createShortcut();
|
||||
|
||||
@@ -58,7 +64,7 @@ test("delete a topic", async ({ page }) => {
|
||||
await app.goto();
|
||||
const notebooks = await app.goToNotebooks();
|
||||
const notebook = await notebooks.createNotebook(NOTEBOOK);
|
||||
const { topics } = (await notebook?.openNotebook()) || {};
|
||||
const topics = await notebook?.openNotebook();
|
||||
const topic = await topics?.findItem({ title: NOTEBOOK.topics[0] });
|
||||
|
||||
await topic?.deleteWithNotes();
|
||||
@@ -72,7 +78,7 @@ test("edit topics individually", async ({ page }) => {
|
||||
await app.goto();
|
||||
const notebooks = await app.goToNotebooks();
|
||||
const notebook = await notebooks.createNotebook(NOTEBOOK);
|
||||
const { topics } = (await notebook?.openNotebook()) || {};
|
||||
const topics = await notebook?.openNotebook();
|
||||
|
||||
const editedTopics: Item[] = [];
|
||||
for (const title of NOTEBOOK.topics) {
|
||||
@@ -92,7 +98,7 @@ test("delete all notes within a topic", async ({ page }) => {
|
||||
await app.goto();
|
||||
const notebooks = await app.goToNotebooks();
|
||||
const notebook = await notebooks.createNotebook(NOTEBOOK);
|
||||
const { topics } = (await notebook?.openNotebook()) || {};
|
||||
const topics = await notebook?.openNotebook();
|
||||
const topic = await topics?.findItem({ title: NOTEBOOK.topics[0] });
|
||||
let notes = await topic?.open();
|
||||
for (let i = 0; i < 2; ++i) {
|
||||
@@ -110,7 +116,7 @@ test("delete all notes within a topic", async ({ page }) => {
|
||||
});
|
||||
|
||||
test(`sort topics`, async ({ page }, info) => {
|
||||
info.setTimeout(1 * 60 * 1000);
|
||||
info.setTimeout(2 * 60 * 1000);
|
||||
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
@@ -119,24 +125,27 @@ test(`sort topics`, async ({ page }, info) => {
|
||||
...NOTEBOOK,
|
||||
topics: ["title1", "title2", "title3", "title4", "title5"]
|
||||
});
|
||||
const { topics } = (await notebook?.openNotebook()) || {};
|
||||
const topics = await notebook?.openNotebook();
|
||||
|
||||
for (const sortBy of sortByOptions) {
|
||||
for (const orderBy of orderByOptions) {
|
||||
await test.step(`sort by ${sortBy}, order by ${orderBy}`, async () => {
|
||||
const sortResult = await topics?.sort({
|
||||
orderBy,
|
||||
sortBy
|
||||
for (const groupBy of groupByOptions) {
|
||||
for (const sortBy of sortByOptions) {
|
||||
for (const orderBy of orderByOptions) {
|
||||
await test.step(`group by ${groupBy}, sort by ${sortBy}, order by ${orderBy}`, async () => {
|
||||
const sortResult = await topics?.sort({
|
||||
groupBy,
|
||||
orderBy,
|
||||
sortBy
|
||||
});
|
||||
if (!sortResult) return;
|
||||
|
||||
expect(await topics?.isEmpty()).toBeFalsy();
|
||||
});
|
||||
if (!sortResult) return;
|
||||
|
||||
expect(await topics?.isEmpty()).toBeFalsy();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test.skip("search topics", async ({ page }) => {
|
||||
test("search topics", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notebooks = await app.goToNotebooks();
|
||||
@@ -146,7 +155,7 @@ test.skip("search topics", async ({ page }) => {
|
||||
});
|
||||
await notebook?.openNotebook();
|
||||
|
||||
const search = await app.search("1", "topics");
|
||||
const search = await app.search("1");
|
||||
const topic = await search?.findItem({ title: "title1" });
|
||||
|
||||
expect((await topic?.getTitle()) === "title1").toBeTruthy();
|
||||
|
||||
1
apps/web/src/assets/attachment.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 782 701.9"><path fill="#f2f2f2" d="m400.5 1.5-25.4 6.6L61.6 88.9 36 95.5a48.2 48.2 0 0 0-34.6 58.6L112 582a48.2 48.2 0 0 0 58.6 34.6l364.3-94a48.2 48.2 0 0 0 34.7-58.6L459 36a48.2 48.2 0 0 0-58.6-34.6Z"/><path fill="#fff" d="m404 15-30.2 7.7-304 78.4-30.2 7.8a34.3 34.3 0 0 0-24.7 41.8l110.4 427.8a34.3 34.3 0 0 0 41.7 24.7h.1l364.3-94a34.3 34.3 0 0 0 24.7-41.7l-110.4-428A34.3 34.3 0 0 0 404 15Z"/><path fill="#f2f2f2" d="M381.2 153.5 197 201a8 8 0 0 1-4-15.5L377.2 138a8 8 0 0 1 4 15.5Zm38.8 17.9-216.3 55.8a8 8 0 1 1-4-15.5L416 155.9a8 8 0 0 1 4 15.5Zm-8.5 99.5-184.3 47.5a8 8 0 1 1-4-15.5l184.3-47.5a8 8 0 1 1 4 15.5Zm38.7 17.9L234 344.6a8 8 0 1 1-4-15.5l216.2-55.8a8 8 0 0 1 4 15.5Zm-8.5 99.5-184.2 47.5a8 8 0 0 1-4-15.5l184.2-47.6a8 8 0 0 1 4 15.6Zm38.8 17.9L264.2 462a8 8 0 1 1-4-15.6l216.3-55.7a8 8 0 0 1 4 15.5Z"/><path fill="#e6e6e6" d="m165.5 249.7-65.2 16.9a3.8 3.8 0 0 1-4.7-2.8l-15-58a3.8 3.8 0 0 1 2.8-4.7l65.2-16.8a3.8 3.8 0 0 1 4.7 2.7l15 58a3.8 3.8 0 0 1-2.8 4.7Zm30.2 117.4L130.5 384a3.8 3.8 0 0 1-4.6-2.7l-15-58a3.8 3.8 0 0 1 2.7-4.8l65.3-16.8a3.8 3.8 0 0 1 4.6 2.8l15 58a3.8 3.8 0 0 1-2.8 4.7ZM226 484.5l-65.2 16.8a3.8 3.8 0 0 1-4.7-2.7l-15-58.1a3.8 3.8 0 0 1 2.8-4.7l65.2-16.8a3.8 3.8 0 0 1 4.7 2.8l15 58a3.8 3.8 0 0 1-2.8 4.7ZM654.7 110H278.3a48.2 48.2 0 0 0-48 48.1V600a48.2 48.2 0 0 0 48 48.1h376.4a48.2 48.2 0 0 0 48-48.1V158a48.2 48.2 0 0 0-48-48.1Z"/><path fill="#fff" d="M654.7 123.8H278.3a34.3 34.3 0 0 0-34.2 34.3V600a34.3 34.3 0 0 0 34.2 34.3h376.4a34.3 34.3 0 0 0 34.2-34.3V158a34.3 34.3 0 0 0-34.2-34.3Z"/><circle cx="694.2" cy="614" r="87.9" fill="#323232"/><path fill="#fff" d="M736.2 602.6h-30.5V572a11.5 11.5 0 0 0-23 0v30.6h-30.5a11.5 11.5 0 0 0 0 22.9h30.5V656a11.5 11.5 0 1 0 23 0v-30.5h30.5a11.5 11.5 0 0 0 0-23Z"/><path fill="#e6e6e6" d="M598 366.7H407.7a8 8 0 1 1 0-16H598a8 8 0 0 1 0 16Zm33 27H407.8a8 8 0 1 1 0-16h223.4a8 8 0 1 1 0 16Zm-33 94.2H407.7a8 8 0 1 1 0-16H598a8 8 0 0 1 0 16Zm33 27.1H407.8a8 8 0 1 1 0-16.1h223.4a8 8 0 1 1 0 16ZM365 406h-67.3a3.8 3.8 0 0 1-3.8-3.9v-60a3.8 3.8 0 0 1 3.8-3.8h67.4a3.8 3.8 0 0 1 3.8 3.9v60a3.8 3.8 0 0 1-3.8 3.8Zm0 121.2h-67.3a3.8 3.8 0 0 1-3.8-3.8v-60a3.8 3.8 0 0 1 3.8-3.8h67.4a3.8 3.8 0 0 1 3.8 3.8v60a3.8 3.8 0 0 1-3.8 3.8Z"/><path fill="#ccc" d="M598.2 231.7H458a8 8 0 0 1 0-16h140.3a8 8 0 0 1 0 16Zm33.1 27.1H457.9a8 8 0 1 1 0-16h173.4a8 8 0 0 1 0 16Z"/><path fill="#323232" d="M426.9 291.5H297.5a3.8 3.8 0 0 1-3.8-3.8v-101a3.8 3.8 0 0 1 3.8-3.8H427a3.8 3.8 0 0 1 3.8 3.9v100.9a3.8 3.8 0 0 1-3.8 3.8Z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
1
apps/web/src/assets/fav.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 922 747"><defs/><path fill="#3f3d56" d="M282 237c-3 101 132 204 299 204s435-199 308-204c-244-9-328-111-303-182 43-129-299-16-304 182z"/><path fill="var(--primary)" d="M505 157l-2-1 1-2-2 2-2-2v1l2 1-1 2v1l2-2 2 1v-1zm152 148l-2-1 1-2-2 2-2-2v1l2 1-1 2v1l2-2 2 1v-1zm-246-18l-2-1 1-2-2 2-2-2v1l2 1-1 2v1l2-2 2 1v-1zm184 96l-2-1 1-2-2 2-2-2v1l2 1-1 2v1l2-2 2 1v-1zm-171-37l-2-1 1-2-2 2-2-2v1l2 1-1 2v1l2-2 2 1v-1zm412-49l-2-1 1-2-2 2-2-2v1l2 1-1 2v1l2-2 2 1v-1zM382 176l-2-1 1-2-2 2-2-2v1l2 1-1 2v1l2-2 2 1v-1z"/><path fill="#fff" d="M553 199l-3-2 2-3h-1l-2 3-2-2-1 1 3 1-2 3 1 1 2-3 3 2v-1z"/><circle cx="450" cy="233.7" r="3" fill="var(--primary)"/><circle cx="333" cy="233.7" r="3" fill="var(--primary)"/><circle cx="537" cy="123.7" r="3" fill="var(--primary)"/><circle cx="509" cy="306.7" r="3" fill="var(--primary)"/><circle cx="507" cy="243.7" r="2" fill="var(--primary)"/><circle cx="712" cy="331.7" r="2" fill="#ff6584"/><circle cx="406" cy="222.7" r="2" fill="#ff6584"/><circle cx="531" cy="60.7" r="2" fill="#ff6584"/><circle cx="449" cy="153.7" r="2" fill="var(--primary)"/><circle cx="474" cy="86.7" r="2" fill="#fff"/><circle cx="773" cy="266.7" r="3" fill="var(--primary)"/><circle cx="574" cy="277.7" r="3" fill="var(--primary)"/><circle cx="523" cy="348.7" r="10" fill="var(--primary)"/><circle cx="644" cy="239.7" r="3" fill="#fff"/><path fill="#2f2e41" d="M92 721s-6 23-1 24l37 2c7 1 18-1 18-6s-13-7-13-7l-19-11-22-2z"/><path fill="#2f2e41" d="M98 553l2 23c0 2-11 98-9 117s0 26 0 30 20 2 20 2l21-134-11-41z"/><path fill="#2f2e41" d="M99 726s-5 18 0 19 30-1 36 0 19 1 19-3-13-8-13-8l-20-11c-1-1-22 3-22 3z"/><path fill="#2f2e41" d="M92 529s-7 18 2 27 18 27 18 29-17 122-15 130 2 12 2 14 6 8 13 8 18-6 19-11-1-47-1-54 25-135 22-138-60-5-60-5z"/><circle cx="138.7" cy="368.6" r="20.7" fill="#9f616a"/><path fill="#9f616a" d="M126 379s4 22 1 25 23 6 23 6l4-33c1-3-28 2-28 2z"/><path fill="var(--primary)" d="M154 408s4-21-26-15c0 0-27 22-26 43s-10 80-8 85-8 9-4 11 69 13 71 0 0-45 1-51 0-57-4-63-4-10-4-10z"/><path fill="#9f616a" d="M141 557s-9 34 3 34 10-36 10-36z"/><path fill="#2f2e41" d="M147 372c0 2-3 2-7 2a33 33 0 01-5-1c-2 0-4 2-5 5l-2 13c-9 5-36-28-22-37 4-3 21-15 32-12s7 16 7 16l1 4a63 63 0 011 9 4 4 0 010 1z"/><path fill="var(--primary)" d="M148 414s-20-1-22 29-1 59-1 60 8 58 16 58 14-1 16-4-5-47-5-47-3-46 2-61 5-33-6-35z"/><path d="M125 452l-6 65 20 45-13-46-1-65m29 8l2 76-6-41 4-35z" opacity=".1" style="isolation:isolate"/><path fill="#3f3d56" d="M258 747H0v-2h258v2z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
1
apps/web/src/assets/monographs.svg
Normal file
|
After Width: | Height: | Size: 12 KiB |
1
apps/web/src/assets/notebook.svg
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
1
apps/web/src/assets/reminder.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 790 512.21"><path fill="#e6e6e6" d="M720.56 510.7 698 442.6s24.82 24.82 24.82 45.18l-4.46-47.09s12.73 17.18 11.46 43.28-9.26 26.72-9.26 26.72ZM236.02 448.7 214 382.23s24.22 24.22 24.22 44.1l-4.35-45.96s12.43 16.76 11.18 42.23-9.03 26.08-9.03 26.08Z"/><path fill="#e6e6e6" d="M579.73 479.36c.03 43.71-86.67 30.27-192.81 30.36s-191.54 13.69-191.58-30.03 86.64-53.3 192.78-53.39 191.57 9.34 191.6 53.06Z"/><path fill="#3f3d56" d="M0 509.69h790v2H0z"/><path fill="#a0616a" d="M505.34 420.32h-13.88l-6.6-53.52h20.48v53.52z"/><path fill="#2f2e41" d="M480 416.36h28.31v16.85H464.7v-1.54a15.3 15.3 0 0 1 15.3-15.31Z"/><path fill="#a0616a" d="M607.34 499.32h-13.88l-6.6-53.52h20.48v53.52z"/><path fill="#2f2e41" d="M582 495.36h28.31v16.85H566.7v-1.54a15.3 15.3 0 0 1 15.3-15.31Z"/><path fill="#a0616a" d="M671.34 340.3a10.32 10.32 0 0 0-2.9-15.54l-32.22-131.3-20.6 8.87 38.33 126.95a10.37 10.37 0 0 0 17.4 11.03ZM646.2 74.96a11.38 11.38 0 0 0-17.4 1.16l-49.9 5.72 7.6 19.24 45.36-8.49a11.44 11.44 0 0 0 14.35-17.63Z"/><path fill="#2f2e41" d="m564 326.7 21.77 163.37 27.1-5.58s-4-118.98 9.55-133.33S605 311.7 605 311.7Z"/><path fill="#2f2e41" d="m573 281.7-10 15s-77-32-77 19-4.4 85.6-6 88 18.44 8.59 28 7c0 0 11.8-82.23 11-87 0 0 75.53 37.03 89.88 33.84s17.73-14.47 20.12-20.85-1-57-1-57l-47.81-14.59Z"/><path fill="#ccc" d="m574.35 191.63-2.85-3.42s-31.92-71.83-19.38-91.2 67.26-22.24 68.97-21.1-4.08 15.94-.09 22.78c0 0-42.4 9.2-45.24 10.33s21.96 43.28 21.96 43.28l-2.85 25.65Z"/><path fill="#ccc" d="M630.22 156.29s-29.65 3.42-30.79 3.42-1.7-7.41-1.7-7.41L571.5 188.2s-12.92 104.2-9.5 102.48 66.5 8.11 67.08 3.55-.57-27.36 1.14-28.5 29.64-71.83 29.64-71.83-2.85-14.82-12.54-19.95-17.1-17.67-17.1-17.67Z"/><path fill="#ccc" d="m650.74 184.22 9.12 9.7S673.4 305.26 666 308.68s-22 3-22 3l-14.35-52.79Z"/><circle cx="601.73" cy="123" r="26.24" fill="#a0616a"/><path fill="#2f2e41" d="M595.57 127.1c-.35-5.45-7.22-5.64-12.68-5.7s-11.97.15-15.06-4.35c-2.04-2.97-1.65-7.1.03-10.28s4.46-5.64 7.19-8c7.04-6.09 14.3-12.13 22.75-16.03s18.36-5.47 27.13-2.34c10.77 3.84 25.33 23.62 26.59 34.99s-3.29 22.95-10.95 31.45-25.18 5.06-36.2 8.08c6.7-9.49 2.28-26.73-8.46-31.16Z"/><circle cx="361.72" cy="403.5" r="62.99" fill="var(--primary)"/><path fill="var(--primary)" d="M319.66 336.04a45.16 45.16 0 0 1-41.26-26.79L178.45 84.16A59.83 59.83 0 1 1 290.32 42.3l72.38 235.4a45.08 45.08 0 0 1-43.04 58.34Z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
1
apps/web/src/assets/search.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 1121 778"><defs/><circle cx="212.6" cy="103" r="64" fill="#ff6584"/><path fill="#f2f2f2" d="M524 343c0 151-90 204-201 204s-200-53-200-204S323 0 323 0s201 192 201 343z"/><path fill="#3f3d56" d="M316 524l2-127 86-156-85 137v-57l59-113-58 98 1-103 63-90-62 74 1-187-7 248 1-10-64-98 63 118-6 114-1-3-73-104 73 114v15h-1v1l-15 290h21l2-150 73-114-73 103z"/><path fill="#f2f2f2" d="M1121 405c0 124-74 167-165 167s-164-43-164-167 164-281 164-281 165 157 165 281z"/><path fill="#3f3d56" d="M950 553l2-104 70-128-70 112 1-46 48-93-48 80 2-83 51-74-51 61 1-154-6 203 1-8-53-81 52 97-5 93v-2l-60-85 60 94-1 11v1l-12 237h16l2-122 61-93-61 84z"/><ellipse cx="554.6" cy="680.5" fill="#3f3d56" rx="554.6" ry="28"/><ellipse cx="892.4" cy="726.8" fill="#3f3d56" rx="95" ry="4.8"/><ellipse cx="548.7" cy="773.1" fill="#3f3d56" rx="95" ry="4.8"/><ellipse cx="287.9" cy="734.3" fill="#3f3d56" rx="217" ry="11"/><circle cx="97.1" cy="566.3" r="79" fill="#2f2e41"/><path fill="#2f2e41" d="M60 628h24v43H60zm48 0l24 1v43l-24-1z"/><ellipse cx="119.5" cy="732.6" fill="#2f2e41" rx="7.5" ry="20" transform="rotate(-89 69 722)"/><ellipse cx="167.6" cy="732.2" fill="#2f2e41" rx="7.5" ry="20" transform="rotate(-89 117 722)"/><circle cx="99.3" cy="546.3" r="27" fill="#fff"/><circle cx="99.3" cy="546.3" r="9" fill="#3f3d56"/><path fill="var(--primary)" d="M21 492c-6-29 15-57 47-64s62 11 68 40-15 39-47 45-62 8-68-21z"/><path fill="var(--primary)" d="M218 610c0 55-33 75-74 75h-2l-6-1c-36-2-65-22-65-74 0-53 68-120 73-125s74 70 74 125z"/><path fill="#3f3d56" d="M142 676l27-37-27 41v5l-6-1 3-55v-5l-27-42 27 38v1l3-42-23-43 23 36 2-87v69l23-27-23 32-1 38 21-36-21 41v21l31-50-31 57z"/><circle cx="712.5" cy="565.4" r="79" fill="#2f2e41"/><path fill="#2f2e41" d="M696 635l23-7 13 41-23 7zm46-14l23-7 13 41-23 7z"/><ellipse cx="767.9" cy="732" fill="#2f2e41" rx="20" ry="7.5" transform="rotate(-17 545 834)"/><ellipse cx="813.5" cy="716.9" fill="#2f2e41" rx="20" ry="7.5" transform="rotate(-17 590 819)"/><circle cx="708.5" cy="545.7" r="27" fill="#fff"/><circle cx="708.5" cy="545.7" r="9" fill="#3f3d56"/><path fill="var(--primary)" d="M618 518c-15-26-4-59 24-75s63-9 77 17-2 41-30 57-57 26-71 1zm-46 82c0 51-30 68-68 68h-7c-34-2-60-21-60-68 0-48 63-110 67-114v-1s68 65 68 115z"/><path fill="#3f3d56" d="M502 661l25-35-25 38v4h-5l2-50v-1l1-5-25-38 25 35v1l2-38-21-40 21 33 2-79v-1 63l21-25-21 31-1 34 20-33-20 38v19l28-46-28 52z"/><path fill="var(--primary)" d="M836 621c0 55-33 75-74 75h-2l-6-1c-36-2-65-22-65-74 0-53 68-120 73-125s74 70 74 125z"/><path fill="#3f3d56" d="M760 687l27-37-27 41v5l-6-1 3-55v-5l-27-42 27 38v1l3-42-23-43 23 36 2-87v69l23-27-23 32-1 38 21-36-21 41v21l31-50-31 57z"/><ellipse cx="721.5" cy="656.8" fill="#2f2e41" rx="12.4" ry="39.5" transform="rotate(-65 653 658)"/><ellipse cx="112.5" cy="651.8" fill="#2f2e41" rx="12.4" ry="39.5" transform="rotate(-68 48 651)"/></svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
1
apps/web/src/assets/tag.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 922 727"><defs/><ellipse cx="211" cy="709.2" fill="#f2f2f2" rx="141" ry="17"/><ellipse cx="508" cy="709.2" fill="#f2f2f2" rx="141" ry="17"/><path fill="#3f3d56" d="M692 408l12-74-154-33 6-190h-75l-6 174-207-44 4-130h-75l-4 114-151-32-13 75 162 34-5 147-174-37-12 75 183 39-6 183h75l6-168 207 44-4 124h75l4-108 122 26 13-74-132-28 5-148zM468 509l-207-44 4-147 208 44z"/><path d="M489 509l4-147-207-44v4l187 40-5 143 21 4zm7-224l6-174h-21l-6 170 21 4zm72 92v5l124 26v-4l-124-27zM214 225l4-114h-21l-4 110 21 4zm347 376v5l101 21 1-4-102-22zm-282-60v5l186 39-4 124h21l4-124-207-44zm-93-96l21 4 4-147-161-34 12-70-20-5-13 75 162 34-5 143zM21 487l11-70-20-5-12 75 183 39-6 183h21l6-183-183-39z" opacity=".2"/><path fill="#2f2e41" d="M734 116h76v131h-76z"/><path fill="#2f2e41" d="M826 111c-9 24-21 47-47 47s-45-21-47-47c-2-33 21-47 47-47 31 0 56 18 47 47z"/><path fill="#ffb9b9" d="M748 294l-27 102s-36 9-32 18 42-6 42-6l35-85zm22 335s24 24 9 41h32l-21-52z"/><path fill="#2f2e41" d="M795 669s0-11-11-4l-43 23s-23 13 10 15 66-6 66-10l-4-24s-7 7-18 0z"/><path fill="#ffb9b9" d="M799 649l-1 43 24 3v-41l-23-5z"/><circle cx="776" cy="132.4" r="34.7" fill="#ffb9b9"/><path fill="#ffb9b9" d="M764 160s17 39 4 51 19 0 19 0 17-24 30-25c0 0-20-11-18-38z"/><path fill="#2f2e41" d="M741 347l-2 61s-47 120-29 149 61 79 61 79 24-11 24-18c0 0-24-60-45-74 0 0 2-25 21-50l18 63 8 96s21 8 29 4c0 0 17-62 5-98l-2-99s33-49-9-106l-7-21s-65 6-72 14zm61 338s0-13-12-5l-49 28s-25 16 12 18 73-6 73-11l-4-30s-7 9-20 0z"/><path fill="var(--primary)" d="M774 208l41-24s59 2 56 33c0 0-12 39-16 36l-8-8s-28 58-28 69a70 70 0 01-4 22l-69 10s8-43-4-51-15-28-4-46a149 149 0 0012-22 39 39 0 0121-21z"/><path fill="#ffb9b9" d="M864 213s68 55 57 67-75 57-75 57-6 39-19 38 2-48 2-48l54-51-29-31z"/><circle cx="778.9" cy="55.5" r="18.4" fill="#2f2e41"/><path fill="#2f2e41" d="M757 46a18 18 0 0116-19h-1a18 18 0 100 37h1a18 18 0 01-16-18zm1 49s-1-2-16 4-24 47-16 72c3 10 0 17-4 22-15 18-9 46 13 56l2 1 9 5s-12-15-6-36a104 104 0 002-53l-2-9c-8-26-4-23 22-49 0 0 15 11 29 17s24 11 15 30-32 35-17 60 17 15 15 24l-2 10 11-11-7 15s24 0 32-19l3 4s16-27-13-51c0 0-9-6 4-25s3-59-14-67-60 0-60 0z"/><path fill="#f2f2f2" d="M386 41l-19-8-19-7 16-13 16-13 3 20 3 21zm-15 143l-19-8-19-7 15-11 15-10 4 18 4 18zm-33 193l-19-8-19-7 16-13 16-13 3 20 3 21zM138 594l-18 10-18 10 1-21v-20l18 11 17 10zm-18-460l-18 10-18 10 1-21v-20l18 11 17 10zm92 544c12 21 37 29 37 29s5-26-7-47-38-29-38-29-4 26 8 47z"/><path fill="var(--primary)" d="M220 670c21 12 30 37 30 37s-26 5-47-6-30-37-30-37 25-5 47 6zm384-10c-14 20-12 46-12 46s26-6 40-25 12-46 12-46-26 6-40 25z"/><path fill="var(--primary)" d="M614 665c-2 24-21 42-21 42s-16-21-13-46 21-42 21-42 15 21 13 46zm241-463l-5 38 5 12s22-27 15-42-15-8-15-8z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
1
apps/web/src/assets/trash.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 920.3 515.1"><defs/><path fill="#e6e6e6" d="M193.7 513.1H53s-2.9-41.8 14-42.2 15 18.5 36-7.6 46.9-24.7 50.2-9.3-6.4 27.8 11.4 24 43.5 6.2 29 35.1z" data-name="Path 1"/><path fill="#fff" d="M121 513.1h-.5c1.1-27.5 6.9-45.2 11.5-55.2a50 50 0 0110-15.3l.3.4s-4.8 4.4-9.8 15.2c-4.7 10-10.4 27.5-11.5 55z" data-name="Path 2"/><path fill="#fff" d="M171.9 513.2l-.5-.1a73.7 73.7 0 0118.6-30.9l.3.3a73.2 73.2 0 00-18.4 30.7z" data-name="Path 3"/><path fill="#fff" d="M75.2 513.2l-.4-.1a51.7 51.7 0 00-4.7-30.2 41.6 41.6 0 00-7.3-10.8l.3-.3a42.2 42.2 0 017.4 10.8 52.2 52.2 0 014.7 30.6z" data-name="Path 4"/><path fill="#2f2e41" d="M212.8 513.8H37.9v-.6c-.1-1.5-2.4-36.3 8.9-48.7a12.3 12.3 0 019-4.3c7.4-.2 12 2.6 15.7 4.8 7 4.1 11.6 6.9 28.5-14 18.2-22.5 38-27.4 49.2-24.7a17.6 17.6 0 0113.9 13.6c1.4 6.4 1 12.5.6 17.4-.4 5.3-.7 9.4 1.4 11.3 1.8 1.6 5.4 1.7 11.3.4 12-2.5 28.8-.3 37.2 10.5 4.5 5.9 8 16.5-.7 34zM39 512.5H212c6.5-13.2 6.7-24.3.6-32.2-7.8-10-24-12.6-36-10-6.3 1.3-10.2 1.1-12.3-.8-2.6-2.2-2.3-6.7-2-12.3a59 59 0 00-.5-17c-1.3-6.4-6-11-12.9-12.6-11-2.6-30 2.2-48 24.3-17.5 21.6-22.7 18.5-30 14.2-3.8-2.3-8.1-4.8-15-4.6a11.1 11.1 0 00-8.2 3.9c-10 11.1-8.8 42.4-8.6 47z" data-name="Path 5"/><path fill="#3f3d56" d="M784 514.5H611.9v-1l-8.2-235.2h188.7zm-170.2-2H782l8.1-232.2H605.7z" data-name="Path 8"/><g fill="#3f3d56" data-name="Group 1"><path d="M639.8 321.9h13.1V484h-13.1z" data-name="Rectangle 17"/><path d="M691.4 321.9h13.1V484h-13.1z" data-name="Rectangle 18"/><path d="M743 321.9h13.1V484H743z" data-name="Rectangle 19"/></g><path fill="#3f3d56" d="M901.7 347.4l-.8-.6-214-135.1 18.6-29.4.9.5L920.3 318zM689.7 211l211.4 133.6 16.4-26-211.3-133.5z"/><path fill="#3f3d56" d="M850.1 200.8a38.5 38.5 0 00-58.6 38l10.2 6.5a30.3 30.3 0 1129 18.3l10.2 6.5a38.5 38.5 0 009.2-69.3z" data-name="Path 10"/><path fill="#3f3d56" d="M0 513.1h909v2H0z" data-name="Rectangle 21"/><g data-name="Group 6"><path fill="#feb8b8" d="M397 499.4h-14.5l-7-56.1H397z" data-name="Path 111"/><path fill="#2f2e41" d="M354.4 513h45.8v-17.7h-28.4a17.4 17.4 0 00-17.4 17.4z" data-name="Path 112"/><path fill="#feb8b8" d="M392 412.4l10 10.5 45.4-33.7-14.8-15.6z" data-name="Path 113"/><path fill="#2f2e41" d="M412.3 427.8l-19.5-20.5-12.8 12.2 31.5 33.1.2-.2a17.4 17.4 0 00.6-24.6z" data-name="Path 114"/><path fill="#feb8b8" d="M290.5 258.5a12 12 0 015.8-17.4l57.1-145.7 22.3 13.3-63.5 139.8a12 12 0 01-21.7 10z" data-name="Path 115"/><path fill="#feb8b8" d="M507.6 269a12 12 0 01-10.7-15l-84.4-131.8 23.9-10.2 75.8 133.5a12 12 0 01-4.6 23.4z" data-name="Path 116"/><path fill="#2f2e41" d="M354 243.9l15 222 35.6-3.2 7.4-163.8 19.9 70.4 43 3.1-17-139z" data-name="Path 117"/><path fill="#2f2e41" d="M438.2 358.8l-6.3 10.5-44 30.4 31.4 16.8s60.9-33.6 55.6-44z" data-name="Path 118"/><path fill="#ccc" d="M322.8 154.1l12.4-35a62.5 62.5 0 0132.3-35.6 89.4 89.4 0 0152.5-3l4.5 1.2a87.4 87.4 0 0133.1 16c7.7 6 14.6 13.7 15.2 21.9 2.1 9.3 3.2 57.6 3.2 57.6h-18.7l3 65-.3-.4s-107.9 20.4-107.9 10v-67.2l-2.2-24.4z" data-name="Path 119"/><circle cx="423.4" cy="41.6" r="29.9" fill="#feb8b8" data-name="Ellipse 12"/><path fill="#2f2e41" d="M428 28.2l23.1 1c3 0 6.1-.2 8.3-2 3.4-3 2.8-8.3 1-12.3C455.4 3.7 444.3-.3 432 0s-25 4.5-31.6 14.8-8.4 23.4-6 35.4a38.5 38.5 0 0131.6-22z" data-name="Path 120"/></g><g data-name="Group 4"><path fill="var(--primary)" d="M565.7 320.5l-84-58.8a3.6 3.6 0 01-.9-5l66-94.1a3.6 3.6 0 015-.9l84 58.9a3.6 3.6 0 01.9 5l-66 94a3.6 3.6 0 01-5 1z" data-name="Path 81"/><path fill="#fff" d="M584.6 256.7L535.3 222a5.3 5.3 0 116.1-8.7l49.3 34.6a5.3 5.3 0 01-6 8.7z" data-name="Path 82"/><path fill="#fff" d="M573.3 272.8L524 238.3a5.3 5.3 0 116.1-8.7l49.3 34.5a5.3 5.3 0 01-6.1 8.7z" data-name="Path 83"/><path fill="#fff" d="M561.9 289.1l-49.3-34.5a5.3 5.3 0 116-8.7l49.4 34.5a5.3 5.3 0 01-6.1 8.7z" data-name="Path 84"/><path fill="#fff" d="M584.5 224.7l-19.1-13.3a5.3 5.3 0 116-8.7l19.2 13.4a5.3 5.3 0 01-6.1 8.6z" data-name="Path 85"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ReactDOM from "react-dom";
|
||||
import { Dialogs } from "../components/dialogs";
|
||||
import { hardNavigate } from "../navigation";
|
||||
import ThemeProvider from "../components/theme-provider";
|
||||
import qclone from "qclone";
|
||||
import { store as notebookStore } from "../stores/notebook-store";
|
||||
@@ -28,7 +29,7 @@ import { store as editorStore } from "../stores/editor-store";
|
||||
import { store as noteStore } from "../stores/note-store";
|
||||
import { db } from "./db";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { Text } from "@theme-ui/components";
|
||||
import { Flex, Text } from "@theme-ui/components";
|
||||
import * as Icon from "../components/icons";
|
||||
import Config from "../utils/config";
|
||||
import { formatDate } from "@notesnook/core/utils/date";
|
||||
@@ -40,11 +41,10 @@ import { FeatureKeys } from "../components/dialogs/feature-dialog";
|
||||
import { AuthenticatorType } from "../components/dialogs/mfa/types";
|
||||
import { Suspense } from "react";
|
||||
import { Reminder } from "@notesnook/core/collections/reminders";
|
||||
import { ConfirmDialogProps } from "../components/dialogs/confirm";
|
||||
|
||||
type DialogTypes = typeof Dialogs;
|
||||
type DialogIds = keyof DialogTypes;
|
||||
export type Perform<T = boolean> = (result: T) => void;
|
||||
export type Perform = (result: boolean) => void;
|
||||
type RenderDialog<TId extends DialogIds, TReturnType> = (
|
||||
dialog: DialogTypes[TId],
|
||||
perform: (result: TReturnType) => void
|
||||
@@ -154,15 +154,26 @@ export function showBuyDialog(plan?: Period, couponCode?: string) {
|
||||
));
|
||||
}
|
||||
|
||||
export function confirm<TCheckId extends string>(
|
||||
props: Omit<ConfirmDialogProps<TCheckId>, "onClose">
|
||||
) {
|
||||
return showDialog<"Confirm", false | Record<TCheckId, boolean>>(
|
||||
"Confirm",
|
||||
(Dialog, perform) => (
|
||||
<Dialog {...props} onClose={(result) => perform(result)} />
|
||||
)
|
||||
);
|
||||
type ConfirmDialogProps = {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
message?: string | JSX.Element;
|
||||
yesText?: string;
|
||||
noText?: string;
|
||||
yesAction?: () => void;
|
||||
width?: string;
|
||||
};
|
||||
export function confirm(props: ConfirmDialogProps) {
|
||||
return showDialog("Confirm", (Dialog, perform) => (
|
||||
<Dialog
|
||||
{...props}
|
||||
onNo={() => perform(false)}
|
||||
onYes={() => {
|
||||
if (props.yesAction) props.yesAction();
|
||||
perform(true);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
export function showPromptDialog(props: {
|
||||
@@ -203,26 +214,41 @@ export function showToolbarConfigDialog() {
|
||||
}
|
||||
|
||||
export function showError(title: string, message: string) {
|
||||
return confirm({ title, message, positiveButtonText: "Okay" });
|
||||
return confirm({ title, message, yesText: "Okay" });
|
||||
}
|
||||
|
||||
export function showMultiDeleteConfirmation(length: number) {
|
||||
return confirm({
|
||||
title: `Delete ${length} items?`,
|
||||
message:
|
||||
"These items will be **kept in your Trash for 7 days** after which they will be permanently deleted.",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
message: (
|
||||
<Text as="span">
|
||||
These items will be{" "}
|
||||
<Text as="span" sx={{ color: "primary" }}>
|
||||
kept in your Trash for 7 days
|
||||
</Text>{" "}
|
||||
after which they will be permanently removed.
|
||||
</Text>
|
||||
),
|
||||
yesText: `Delete selected`,
|
||||
noText: "Cancel"
|
||||
});
|
||||
}
|
||||
|
||||
export function showMultiPermanentDeleteConfirmation(length: number) {
|
||||
return confirm({
|
||||
title: `Permanently delete ${length} items?`,
|
||||
message:
|
||||
"These items will be **permanently deleted**. This is IRREVERSIBLE.",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
message: (
|
||||
<Text as="span">
|
||||
These items will be{" "}
|
||||
<Text as="span" sx={{ color: "primary" }}>
|
||||
permanently deleted
|
||||
</Text>
|
||||
{". "}
|
||||
This action is IRREVERSIBLE.
|
||||
</Text>
|
||||
),
|
||||
yesText: `Permanently delete selected`,
|
||||
noText: "Cancel"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -231,8 +257,8 @@ export function showLogoutConfirmation() {
|
||||
title: `Logout?`,
|
||||
message:
|
||||
"Logging out will delete all local data and reset the app. Make sure you have synced your data before logging out.",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
yesText: `Yes`,
|
||||
noText: "No"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -241,8 +267,8 @@ export function showClearSessionsConfirmation() {
|
||||
title: `Logout from other devices?`,
|
||||
message:
|
||||
"All other logged-in devices will be forced to logout stopping sync. Use with care lest you lose important notes.",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
yesText: `Yes`,
|
||||
noText: "No"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -250,7 +276,9 @@ export function showAccountLoggedOutNotice(reason?: string) {
|
||||
return confirm({
|
||||
title: "You were logged out",
|
||||
message: reason,
|
||||
negativeButtonText: "Okay"
|
||||
noText: "Okay",
|
||||
yesText: `Relogin`,
|
||||
yesAction: () => hardNavigate("/login")
|
||||
});
|
||||
}
|
||||
|
||||
@@ -259,13 +287,24 @@ export function showAppUpdatedNotice(
|
||||
) {
|
||||
return confirm({
|
||||
title: `Welcome to v${version.formatted}`,
|
||||
message: `## Changelog:
|
||||
|
||||
\`\`\`
|
||||
${version.changelog || "No change log."}
|
||||
\`\`\`
|
||||
`,
|
||||
positiveButtonText: `Continue`
|
||||
message: (
|
||||
<Flex
|
||||
bg="bgSecondary"
|
||||
p={1}
|
||||
sx={{ borderRadius: "default", flexDirection: "column" }}
|
||||
>
|
||||
<Text variant="title">Changelog:</Text>
|
||||
<Text
|
||||
as="pre"
|
||||
variant="body"
|
||||
mt={1}
|
||||
sx={{ fontFamily: "monospace", overflow: "auto" }}
|
||||
>
|
||||
{version.changelog || "No change log."}
|
||||
</Text>
|
||||
</Flex>
|
||||
),
|
||||
yesText: `Yay!`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -667,29 +706,32 @@ export function showOnboardingDialog(type: string) {
|
||||
));
|
||||
}
|
||||
|
||||
export async function showInvalidSystemTimeDialog({
|
||||
export function showInvalidSystemTimeDialog({
|
||||
serverTime,
|
||||
localTime
|
||||
}: {
|
||||
serverTime: number;
|
||||
localTime: number;
|
||||
}) {
|
||||
const result = await confirm({
|
||||
return confirm({
|
||||
title: "Your system clock is out of sync",
|
||||
subtitle:
|
||||
"Please correct your system date & time and reload the app to avoid syncing issues.",
|
||||
message: `Server time: ${formatDate(serverTime, {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "medium"
|
||||
})}
|
||||
Local time: ${formatDate(localTime, {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "medium"
|
||||
})}
|
||||
Please sync your system time with [https://time.is](https://time.is).`,
|
||||
positiveButtonText: "Reload app"
|
||||
message: (
|
||||
<>
|
||||
Server time:{" "}
|
||||
{formatDate(serverTime, { dateStyle: "medium", timeStyle: "medium" })}
|
||||
<br />
|
||||
Local time:{" "}
|
||||
{formatDate(localTime, { dateStyle: "medium", timeStyle: "medium" })}
|
||||
<br />
|
||||
Please sync your system time with{" "}
|
||||
<a href="https://time.is">https://time.is/</a>.
|
||||
</>
|
||||
),
|
||||
yesText: "Reload app",
|
||||
yesAction: () => window.location.reload()
|
||||
});
|
||||
if (result) window.location.reload();
|
||||
}
|
||||
|
||||
export async function showUpdateAvailableNotice({
|
||||
@@ -729,18 +771,37 @@ type UpdateDialogProps = {
|
||||
onClick: () => void;
|
||||
};
|
||||
};
|
||||
async function showUpdateDialog({
|
||||
function showUpdateDialog({
|
||||
title,
|
||||
subtitle,
|
||||
changelog,
|
||||
action
|
||||
}: UpdateDialogProps) {
|
||||
const result = await confirm({
|
||||
return confirm({
|
||||
title,
|
||||
subtitle,
|
||||
message: changelog,
|
||||
width: 500,
|
||||
positiveButtonText: action.text
|
||||
message: changelog && (
|
||||
<Flex sx={{ borderRadius: "default", flexDirection: "column" }}>
|
||||
<Text
|
||||
as="div"
|
||||
variant="body"
|
||||
sx={{ overflow: "auto", fontFamily: "body" }}
|
||||
css={`
|
||||
h2 {
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
}
|
||||
`}
|
||||
dangerouslySetInnerHTML={{ __html: changelog }}
|
||||
></Text>
|
||||
</Flex>
|
||||
),
|
||||
width: "500px",
|
||||
yesText: action.text,
|
||||
yesAction: action.onClick
|
||||
});
|
||||
if (result && action.onClick) action.onClick();
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import { EVENTS } from "@notesnook/core/common";
|
||||
|
||||
export const CREATE_BUTTON_MAP = {
|
||||
notes: {
|
||||
title: "Add a note",
|
||||
title: "Make a note",
|
||||
onClick: () =>
|
||||
hashNavigate("/notes/create", { addNonce: true, replace: true })
|
||||
},
|
||||
@@ -49,10 +49,6 @@ export const CREATE_BUTTON_MAP = {
|
||||
title: "Create a notebook",
|
||||
onClick: () => hashNavigate("/notebooks/create", { replace: true })
|
||||
},
|
||||
notebook: {
|
||||
title: "Add a note",
|
||||
onClick: () => hashNavigate(`/notes/create`, { replace: true })
|
||||
},
|
||||
topics: {
|
||||
title: "Create a topic",
|
||||
onClick: () => hashNavigate(`/topics/create`, { replace: true })
|
||||
|
||||
@@ -24,8 +24,8 @@ import { useStore } from "../../stores/attachment-store";
|
||||
import { formatBytes } from "../../utils/filename";
|
||||
import Field from "../field";
|
||||
import ListContainer from "../list-container";
|
||||
import AttachmentsPlaceholder from "../placeholders/attachments-placeholder";
|
||||
import Dialog from "./dialog";
|
||||
import Placeholder from "../placeholders";
|
||||
|
||||
function AttachmentsDialog({ onClose }) {
|
||||
const attachments = useStore((store) => store.attachments);
|
||||
@@ -57,7 +57,7 @@ function AttachmentsDialog({ onClose }) {
|
||||
header={<div />}
|
||||
type="attachments"
|
||||
groupType="attachments"
|
||||
placeholder={<Placeholder context="attachments" />}
|
||||
placeholder={AttachmentsPlaceholder}
|
||||
items={attachments}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
52
apps/web/src/components/dialogs/confirm.js
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
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 { Box, Text } from "@theme-ui/components";
|
||||
import Dialog from "./dialog";
|
||||
|
||||
function Confirm(props) {
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={props.title}
|
||||
icon={props.icon}
|
||||
width={props.width}
|
||||
description={props.subtitle}
|
||||
onClose={() => props.onNo(false)}
|
||||
positiveButton={
|
||||
props.yesText && {
|
||||
text: props.yesText,
|
||||
onClick: () => props.onYes(true),
|
||||
autoFocus: !!props.yesText
|
||||
}
|
||||
}
|
||||
negativeButton={
|
||||
props.noText && { text: props.noText, onClick: () => props.onNo(false) }
|
||||
}
|
||||
>
|
||||
<Box pb={!props.noText && !props.yesText ? 2 : 0}>
|
||||
<Text as="span" variant="body">
|
||||
{props.message}
|
||||
</Text>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default Confirm;
|
||||
@@ -1,120 +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 { Box, Checkbox, Label, Text } from "@theme-ui/components";
|
||||
import { useRef } from "react";
|
||||
import { Perform } from "../../common/dialog-controller";
|
||||
import { mdToHtml } from "../../utils/md";
|
||||
import Dialog from "./dialog";
|
||||
|
||||
type Check = { text: string; default?: boolean };
|
||||
export type ConfirmDialogProps<TCheckId extends string> = {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
onClose: Perform<false | Record<TCheckId, boolean>>;
|
||||
width?: number;
|
||||
positiveButtonText?: string;
|
||||
negativeButtonText?: string;
|
||||
message?: string;
|
||||
checks?: Partial<Record<TCheckId, Check>>;
|
||||
};
|
||||
|
||||
function ConfirmDialog<TCheckId extends string>(
|
||||
props: ConfirmDialogProps<TCheckId>
|
||||
) {
|
||||
const {
|
||||
onClose,
|
||||
title,
|
||||
subtitle,
|
||||
width,
|
||||
negativeButtonText,
|
||||
positiveButtonText,
|
||||
message,
|
||||
checks
|
||||
} = props;
|
||||
const checkedItems = useRef<Record<TCheckId, boolean>>({} as any);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={title}
|
||||
width={width}
|
||||
description={subtitle}
|
||||
onClose={() => onClose(false)}
|
||||
positiveButton={
|
||||
positiveButtonText
|
||||
? {
|
||||
text: positiveButtonText,
|
||||
onClick: () => onClose(checkedItems.current),
|
||||
autoFocus: !!positiveButtonText
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
negativeButton={
|
||||
negativeButtonText
|
||||
? {
|
||||
text: negativeButtonText,
|
||||
onClick: () => onClose(false)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
pb: !negativeButtonText && !positiveButtonText ? 2 : 0,
|
||||
p: { m: 0 }
|
||||
}}
|
||||
>
|
||||
{message ? (
|
||||
<Text
|
||||
as="span"
|
||||
variant="body"
|
||||
dangerouslySetInnerHTML={{ __html: mdToHtml(message) }}
|
||||
/>
|
||||
) : null}
|
||||
{checks
|
||||
? Object.entries<Check | undefined>(checks).map(
|
||||
([id, check]) =>
|
||||
check && (
|
||||
<Label
|
||||
key={id}
|
||||
id={id}
|
||||
variant="text.body"
|
||||
sx={{ alignItems: "center" }}
|
||||
>
|
||||
<Checkbox
|
||||
name={id}
|
||||
defaultChecked={check.default}
|
||||
sx={{ mr: "small", width: 18, height: 18 }}
|
||||
onChange={(e) =>
|
||||
(checkedItems.current[id as TCheckId] =
|
||||
e.currentTarget.checked)
|
||||
}
|
||||
/>
|
||||
{check.text}{" "}
|
||||
</Label>
|
||||
)
|
||||
)
|
||||
: null}
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfirmDialog;
|
||||
@@ -17,7 +17,7 @@ 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 { Flex, Text } from "@theme-ui/components";
|
||||
import { Flex, Link, Text } from "@theme-ui/components";
|
||||
import { appVersion } from "../../utils/version";
|
||||
import Field from "../field";
|
||||
import Dialog from "./dialog";
|
||||
@@ -152,14 +152,33 @@ export default IssueDialog;
|
||||
function showIssueReportedDialog({ url }: { url: string }) {
|
||||
return confirm({
|
||||
title: "Thank you for reporting!",
|
||||
positiveButtonText: "Copy link",
|
||||
message: `You can track your bug report at [${url}](${url}).
|
||||
|
||||
Please note that we will respond to your bug report on the link above. **We recommended that you save the above link for later reference.**
|
||||
|
||||
If your issue is critical (e.g. notes not syncing, crashes etc.), please [join our Discord community](https://discord.com/invite/zQBK97EE22) for one-to-one support.`
|
||||
}).then((result) => {
|
||||
result && clipboard.writeText(url);
|
||||
yesAction: () => clipboard.writeText(url),
|
||||
yesText: "Copy link",
|
||||
message: (
|
||||
<>
|
||||
<p>
|
||||
You can track your bug report at{" "}
|
||||
<Link target="_blank" href={url} sx={{ lineBreak: "anywhere" }}>
|
||||
{url}
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
Please note that we will respond to your bug report on the link above.{" "}
|
||||
<b>
|
||||
We recommended that you save the above link for later reference.
|
||||
</b>
|
||||
</p>
|
||||
<p>
|
||||
If your issue is critical (e.g. notes not syncing, crashes etc.),
|
||||
please{" "}
|
||||
<a href="https://discord.com/invite/zQBK97EE22">
|
||||
join our Discord community
|
||||
</a>{" "}
|
||||
for one-to-one support.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ import { usePersistentState } from "../../hooks/use-persistent-state";
|
||||
type MoveDialogProps = { onClose: Perform; noteIds: string[] };
|
||||
type NotebookReference = {
|
||||
id: string;
|
||||
topic?: string;
|
||||
topic: string;
|
||||
new: boolean;
|
||||
op: "add" | "remove";
|
||||
};
|
||||
@@ -95,19 +95,6 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const notebook of noteIds
|
||||
.map((id) => db.relations?.to({ id, type: "note" }, "notebook"))
|
||||
.flat()) {
|
||||
if (!notebook) continue;
|
||||
|
||||
selected.push({
|
||||
id: notebook.id,
|
||||
op: "add",
|
||||
new: false
|
||||
});
|
||||
}
|
||||
|
||||
setSelected(selected);
|
||||
setIsMultiselect(false);
|
||||
}, [noteIds, notebooks, setSelected, setIsMultiselect]);
|
||||
@@ -141,15 +128,19 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
|
||||
|
||||
notestore.refresh();
|
||||
|
||||
const stringified = stringifySelected(selected);
|
||||
if (stringified) {
|
||||
showToast(
|
||||
"success",
|
||||
`${pluralize(noteIds.length, "note", "notes")} ${stringified
|
||||
.replace("Add", "added")
|
||||
.replace("remove", "removed")}`
|
||||
);
|
||||
}
|
||||
const addedTopics = selected.filter((a) => a.op === "add").length;
|
||||
const removedTopics = selected.filter(
|
||||
(a) => a.op === "remove"
|
||||
).length;
|
||||
|
||||
showToast(
|
||||
"success",
|
||||
`${pluralize(noteIds.length, "note", "notes")} added to ${pluralize(
|
||||
addedTopics,
|
||||
"topic",
|
||||
"topics"
|
||||
)} & removed from ${pluralize(removedTopics, "topic", "topics")}.`
|
||||
);
|
||||
|
||||
onClose(true);
|
||||
}
|
||||
@@ -235,12 +226,10 @@ function NotebookItem(props: {
|
||||
}) {
|
||||
const { notebook, isSearching, onCreateItem } = props;
|
||||
|
||||
const { selected, setIsMultiselect, setSelected, isMultiselect } =
|
||||
useSelectionStore();
|
||||
const { selected, setIsMultiselect, setSelected } = useSelectionStore();
|
||||
|
||||
const [isCreatingNew, setIsCreatingNew] = useState(false);
|
||||
const index = findSelectionIndex(notebook, selected);
|
||||
const isSelected = index > -1;
|
||||
const isSelected = isNotebookSelected(notebook, selected);
|
||||
|
||||
return (
|
||||
<Box as="li" data-test-id="notebook">
|
||||
@@ -274,14 +263,10 @@ function NotebookItem(props: {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const isCtrlPressed = e.ctrlKey || e.metaKey;
|
||||
if (isCtrlPressed) setIsMultiselect(true);
|
||||
|
||||
if (isMultiselect || isCtrlPressed) {
|
||||
setSelected(selectMultiple(notebook, selected));
|
||||
} else {
|
||||
setSelected(selectSingle(notebook, selected));
|
||||
}
|
||||
setIsMultiselect(true);
|
||||
setSelected(
|
||||
selectNotebook(notebook, selected, isSelected || false)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<SelectedCheck size={20} selected={isSelected} />
|
||||
@@ -399,9 +384,9 @@ function TopicItem(props: { topic: Topic }) {
|
||||
if (isCtrlPressed) setIsMultiselect(true);
|
||||
|
||||
if (isMultiselect || isCtrlPressed) {
|
||||
setSelected(selectMultiple(topic, selected));
|
||||
setSelected(selectMultipleTopics(topic, selected));
|
||||
} else {
|
||||
setSelected(selectSingle(topic, selected));
|
||||
setSelected(selectSingleTopic(topic, selected));
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -560,24 +545,22 @@ function SelectedCheck({
|
||||
);
|
||||
}
|
||||
|
||||
function createSelection(topic: Topic | Notebook): NotebookReference {
|
||||
function createSelection(topic: Topic): NotebookReference {
|
||||
return {
|
||||
id: "notebookId" in topic ? topic.notebookId : topic.id,
|
||||
topic: "notebookId" in topic ? topic.id : undefined,
|
||||
id: topic.notebookId,
|
||||
topic: topic.id,
|
||||
op: "add",
|
||||
new: true
|
||||
};
|
||||
}
|
||||
|
||||
function findSelectionIndex(
|
||||
topic: Topic | NotebookReference | Notebook,
|
||||
topic: Topic | NotebookReference,
|
||||
array: NotebookReference[]
|
||||
) {
|
||||
return "op" in topic
|
||||
? array.findIndex((a) => a.id === topic.id && a.topic === topic.topic)
|
||||
: "notebookId" in topic
|
||||
? array.findIndex((a) => a.id === topic.notebookId && a.topic === topic.id)
|
||||
: array.findIndex((a) => a.id === topic.id && !a.topic);
|
||||
: array.findIndex((a) => a.id === topic.notebookId && a.topic === topic.id);
|
||||
}
|
||||
|
||||
function topicHasNotes(topic: Item, noteIds: string[]) {
|
||||
@@ -585,10 +568,36 @@ function topicHasNotes(topic: Item, noteIds: string[]) {
|
||||
return noteIds.some((id) => notes.indexOf(id) > -1);
|
||||
}
|
||||
|
||||
function selectMultiple(
|
||||
topic: Topic | Notebook,
|
||||
selected: NotebookReference[]
|
||||
// There are 3 cases:
|
||||
// 1. Click on a notebook to select/deselect all its topics
|
||||
// 2. Click on a topic to select/deselect it (and deselect all other topics)
|
||||
// 3. Ctrl+click on a topic to enter multi select
|
||||
|
||||
function selectNotebook(
|
||||
notebook: Notebook,
|
||||
selected: NotebookReference[],
|
||||
isNotebookSelected: boolean
|
||||
) {
|
||||
for (const topic of notebook.topics) {
|
||||
const index = findSelectionIndex(topic, selected);
|
||||
const item = selected[index];
|
||||
|
||||
// 1. first reset the item's selection state
|
||||
// 2. set the new state
|
||||
|
||||
if (item?.new) selected.splice(index, 1);
|
||||
else if (item && !item.new) item.op = "remove";
|
||||
|
||||
if (!isNotebookSelected) {
|
||||
if (!item || item.new) selected.push(createSelection(topic));
|
||||
else if (item && !item.new) item.op = "add";
|
||||
}
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
function selectMultipleTopics(topic: Topic, selected: NotebookReference[]) {
|
||||
const index = findSelectionIndex(topic, selected);
|
||||
const isSelected = index > -1;
|
||||
const item = selected[index];
|
||||
@@ -604,7 +613,7 @@ function selectMultiple(
|
||||
return selected;
|
||||
}
|
||||
|
||||
function selectSingle(topic: Topic | Notebook, array: NotebookReference[]) {
|
||||
function selectSingleTopic(topic: Topic, array: NotebookReference[]) {
|
||||
const selected: NotebookReference[] = array.filter((ref) => !ref.new);
|
||||
|
||||
const index = findSelectionIndex(topic, array);
|
||||
@@ -621,6 +630,19 @@ function selectSingle(topic: Topic | Notebook, array: NotebookReference[]) {
|
||||
return selected;
|
||||
}
|
||||
|
||||
function isNotebookSelected(notebook: Notebook, selected: NotebookReference[]) {
|
||||
const selectedTopics = notebook.topics.filter((topic) => {
|
||||
const index = findSelectionIndex(topic, selected);
|
||||
return selected[index]?.op === "add";
|
||||
});
|
||||
|
||||
return !selectedTopics.length
|
||||
? false
|
||||
: selectedTopics.length === notebook.topics.length
|
||||
? true
|
||||
: null;
|
||||
}
|
||||
|
||||
function stringifySelected(suggestion: NotebookReference[]) {
|
||||
const added = suggestion
|
||||
.filter((a) => a.op === "add")
|
||||
@@ -637,21 +659,16 @@ function stringifySelected(suggestion: NotebookReference[]) {
|
||||
if (added.length > 1) parts.push(`and ${added.length - 1} others`);
|
||||
|
||||
if (removed.length >= 1) {
|
||||
parts.push("& remove from");
|
||||
parts.push(removed[0]);
|
||||
parts.push("remove from");
|
||||
parts.push(added[0]);
|
||||
}
|
||||
if (removed.length > 1) parts.push(`and ${removed.length - 1} others`);
|
||||
|
||||
return parts.join(" ") + ".";
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
function resolve(ref: NotebookReference) {
|
||||
const notebook = db.notebooks?.notebook(ref.id);
|
||||
if (!notebook) return undefined;
|
||||
|
||||
if (ref.topic) {
|
||||
return notebook.topics.topic(ref.topic)?._topic?.title;
|
||||
} else {
|
||||
return notebook.title;
|
||||
}
|
||||
const topic = db.notebooks?.notebook(ref.id)?.topics.topic(ref.topic);
|
||||
if (!topic) return undefined;
|
||||
return topic._topic.title;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import * as Icon from "../icons";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { db } from "../../common/db";
|
||||
import { Menu, useMenuTrigger } from "../../hooks/use-menu";
|
||||
import { useMenuTrigger } from "../../hooks/use-menu";
|
||||
import { useStore as useNoteStore } from "../../stores/note-store";
|
||||
import { useStore as useNotebookStore } from "../../stores/notebook-store";
|
||||
import useMobile from "../../hooks/use-mobile";
|
||||
@@ -36,97 +36,84 @@ const groupByToTitleMap = {
|
||||
month: "Month"
|
||||
};
|
||||
|
||||
const groupByMenu = {
|
||||
key: "groupBy",
|
||||
title: "Group by",
|
||||
icon: Icon.GroupBy,
|
||||
items: map([
|
||||
{ key: "none", title: "None" },
|
||||
{ key: "default", title: "Default" },
|
||||
{ key: "year", title: "Year" },
|
||||
{ key: "month", title: "Month" },
|
||||
{ key: "week", title: "Week" },
|
||||
{ key: "abc", title: "A - Z" }
|
||||
])
|
||||
};
|
||||
|
||||
const orderByMenu = {
|
||||
key: "sortDirection",
|
||||
title: "Order by",
|
||||
icon: ({ groupOptions }) =>
|
||||
groupOptions.sortDirection === "asc"
|
||||
? groupOptions.sortBy === "title"
|
||||
? Icon.OrderAtoZ
|
||||
: Icon.OrderOldestNewest
|
||||
: groupOptions.sortBy === "title"
|
||||
? Icon.OrderZtoA
|
||||
: Icon.OrderNewestOldest,
|
||||
items: map([
|
||||
{
|
||||
key: "asc",
|
||||
title: ({ groupOptions }) =>
|
||||
groupOptions.sortBy === "title" ? "A - Z" : "Oldest - newest"
|
||||
},
|
||||
{
|
||||
key: "desc",
|
||||
title: ({ groupOptions }) =>
|
||||
groupOptions.sortBy === "title" ? "Z - A" : "Newest - oldest"
|
||||
}
|
||||
])
|
||||
};
|
||||
|
||||
const sortByMenu = {
|
||||
key: "sortBy",
|
||||
title: "Sort by",
|
||||
icon: Icon.SortBy,
|
||||
items: map([
|
||||
{
|
||||
key: "dateCreated",
|
||||
title: "Date created",
|
||||
hidden: ({ type }) => type === "trash"
|
||||
},
|
||||
{
|
||||
key: "dateEdited",
|
||||
title: "Date edited",
|
||||
hidden: ({ type }) => type === "trash" || type === "tags"
|
||||
},
|
||||
{
|
||||
key: "dateDeleted",
|
||||
title: "Date deleted",
|
||||
hidden: ({ type }) => type !== "trash"
|
||||
},
|
||||
{
|
||||
key: "dateModified",
|
||||
title: "Date modified",
|
||||
hidden: ({ type }) => type !== "tags"
|
||||
},
|
||||
{
|
||||
key: "title",
|
||||
title: "Title",
|
||||
hidden: ({ groupOptions, parent, isUngrouped }, item) => {
|
||||
if (isUngrouped) return false;
|
||||
|
||||
return (
|
||||
parent?.key === "sortBy" &&
|
||||
item.key === "title" &&
|
||||
groupOptions.groupBy !== "abc" &&
|
||||
groupOptions.groupBy !== "none"
|
||||
);
|
||||
const menuItems = [
|
||||
{
|
||||
key: "sortDirection",
|
||||
title: "Order by",
|
||||
icon: ({ groupOptions }) =>
|
||||
groupOptions.sortDirection === "asc"
|
||||
? groupOptions.sortBy === "title"
|
||||
? Icon.OrderAtoZ
|
||||
: Icon.OrderOldestNewest
|
||||
: groupOptions.sortBy === "title"
|
||||
? Icon.OrderZtoA
|
||||
: Icon.OrderNewestOldest,
|
||||
items: map([
|
||||
{
|
||||
key: "asc",
|
||||
title: ({ groupOptions }) =>
|
||||
groupOptions.sortBy === "title" ? "A - Z" : "Oldest - newest"
|
||||
},
|
||||
{
|
||||
key: "desc",
|
||||
title: ({ groupOptions }) =>
|
||||
groupOptions.sortBy === "title" ? "Z - A" : "Newest - oldest"
|
||||
}
|
||||
}
|
||||
])
|
||||
};
|
||||
|
||||
export function showSortMenu(type, refresh) {
|
||||
const groupOptions = db.settings.getGroupOptions(type);
|
||||
Menu.openMenu([orderByMenu, sortByMenu], {
|
||||
title: "Sort",
|
||||
groupOptions,
|
||||
refresh,
|
||||
type,
|
||||
isUngrouped: true
|
||||
});
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
key: "sortBy",
|
||||
title: "Sort by",
|
||||
icon: Icon.SortBy,
|
||||
items: map([
|
||||
{
|
||||
key: "dateCreated",
|
||||
title: "Date created",
|
||||
hidden: ({ type }) => type === "trash"
|
||||
},
|
||||
{
|
||||
key: "dateEdited",
|
||||
title: "Date edited",
|
||||
hidden: ({ type }) => type === "trash" || type === "tags"
|
||||
},
|
||||
{
|
||||
key: "dateDeleted",
|
||||
title: "Date deleted",
|
||||
hidden: ({ type }) => type !== "trash"
|
||||
},
|
||||
{
|
||||
key: "dateModified",
|
||||
title: "Date modified",
|
||||
hidden: ({ type }) => type !== "tags"
|
||||
},
|
||||
{
|
||||
key: "title",
|
||||
title: "Title",
|
||||
hidden: ({ groupOptions, parent }, item) => {
|
||||
return (
|
||||
parent?.key === "sortBy" &&
|
||||
item.key === "title" &&
|
||||
groupOptions.groupBy !== "abc" &&
|
||||
groupOptions.groupBy !== "none"
|
||||
);
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
{
|
||||
key: "groupBy",
|
||||
title: "Group by",
|
||||
icon: Icon.GroupBy,
|
||||
items: map([
|
||||
{ key: "none", title: "None" },
|
||||
{ key: "default", title: "Default" },
|
||||
{ key: "year", title: "Year" },
|
||||
{ key: "month", title: "Month" },
|
||||
{ key: "week", title: "Week" },
|
||||
{ key: "abc", title: "A - Z" }
|
||||
])
|
||||
}
|
||||
];
|
||||
|
||||
function changeGroupOptions({ groupOptions, type, refresh, parent }, item) {
|
||||
if (!parent) return false;
|
||||
@@ -145,10 +132,19 @@ function isChecked({ groupOptions, parent }, item) {
|
||||
return groupOptions[parent.key] === item.key;
|
||||
}
|
||||
|
||||
function isDisabled({ groupOptions, parent }, item) {
|
||||
return (
|
||||
parent?.key === "sortBy" &&
|
||||
item.key === "title" &&
|
||||
groupOptions.groupBy === "abc"
|
||||
);
|
||||
}
|
||||
|
||||
function map(items) {
|
||||
return items.map((item) => {
|
||||
item.checked = isChecked;
|
||||
item.onClick = changeGroupOptions;
|
||||
item.disabled = isDisabled;
|
||||
return item;
|
||||
}, []);
|
||||
}
|
||||
@@ -252,7 +248,7 @@ function GroupHeader(props) {
|
||||
<Flex mr={1}>
|
||||
{type && (
|
||||
<IconButton
|
||||
testId={`${type}-sort-button`}
|
||||
testId="sort-icon-button"
|
||||
icon={
|
||||
groupOptions.sortDirection === "asc"
|
||||
? Icon.SortAsc
|
||||
@@ -263,7 +259,7 @@ function GroupHeader(props) {
|
||||
const groupOptions = db.settings.getGroupOptions(type);
|
||||
setGroupOptions(groupOptions);
|
||||
|
||||
openMenu([orderByMenu, sortByMenu, groupByMenu], {
|
||||
openMenu(menuItems, {
|
||||
title: "Group & sort",
|
||||
groupOptions,
|
||||
refresh,
|
||||
|
||||
@@ -189,13 +189,12 @@ import {
|
||||
mdiMinusCircleOutline,
|
||||
mdiLightbulbOnOutline,
|
||||
mdiNoteMultipleOutline,
|
||||
mdiBookMultipleOutline,
|
||||
mdiArrowTopRight,
|
||||
mdiBookmarkRemoveOutline
|
||||
mdiBookMultipleOutline
|
||||
} from "@mdi/js";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { AnimatedFlex } from "../animated";
|
||||
import { Theme } from "@notesnook/theme";
|
||||
import { Flex, FlexProps } from "@theme-ui/components";
|
||||
import { FlexProps } from "@theme-ui/components";
|
||||
import { MotionProps } from "framer-motion";
|
||||
|
||||
type MDIIconWrapperProps = {
|
||||
@@ -238,7 +237,7 @@ function MDIIconWrapper({
|
||||
);
|
||||
}
|
||||
|
||||
export type IconProps = FlexProps &
|
||||
type IconProps = FlexProps &
|
||||
MotionProps &
|
||||
Omit<MDIIconWrapperProps, "path"> & {
|
||||
hoverColor?: keyof Theme["colors"];
|
||||
@@ -254,8 +253,10 @@ function createIcon(path: string, rotate = false) {
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const { sx, rotate: _rotate = rotate, size, ...restProps } = props;
|
||||
return (
|
||||
<Flex
|
||||
<AnimatedFlex
|
||||
{...restProps}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
sx={{
|
||||
...sx,
|
||||
justifyContent: "center",
|
||||
@@ -274,7 +275,7 @@ function createIcon(path: string, rotate = false) {
|
||||
props.hoverColor && isHovering ? props.hoverColor : props.color
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</AnimatedFlex>
|
||||
);
|
||||
};
|
||||
NNIcon.isReactComponent = true;
|
||||
@@ -291,7 +292,6 @@ export const Notebook2 = createIcon(mdiNotebookOutline);
|
||||
export const ArrowLeft = createIcon(mdiArrowLeft);
|
||||
export const ArrowRight = createIcon(mdiArrowRight);
|
||||
export const ArrowDown = createIcon(mdiArrowDown);
|
||||
export const ArrowTopRight = createIcon(mdiArrowTopRight);
|
||||
export const Move = createIcon(mdiBookPlusMultipleOutline);
|
||||
export const Topic = createIcon(mdiBookmarkOutline);
|
||||
export const Alert = createIcon(mdiAlertOctagonOutline);
|
||||
@@ -307,8 +307,7 @@ export const Check = createIcon(mdiCheck);
|
||||
export const Cross = createIcon(mdiClose);
|
||||
export const MoreVertical = createIcon(mdiDotsVertical);
|
||||
export const Trash = createIcon(mdiTrashCanOutline);
|
||||
export const TopicRemove = createIcon(mdiBookmarkRemoveOutline);
|
||||
export const NotebookRemove = createIcon(mdiBookRemoveOutline);
|
||||
export const TopicRemove = createIcon(mdiBookRemoveOutline);
|
||||
export const Search = createIcon(mdiMagnify);
|
||||
export const Menu = createIcon(mdiMenu);
|
||||
export const Login = createIcon(mdiLoginVariant);
|
||||
|
||||
@@ -20,16 +20,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { toTitleCase, toCamelCase, KebabCase } from "../../utils/string";
|
||||
import * as Icons from "./index";
|
||||
|
||||
export type IconAlias = KebabCase<keyof typeof Icons>;
|
||||
export function getIconFromAlias(alias: IconAlias) {
|
||||
export function getIconFromAlias<T extends KebabCase<keyof typeof Icons>>(
|
||||
alias: T
|
||||
) {
|
||||
if (!alias) return;
|
||||
const iconName = toTitleCase(toCamelCase(alias));
|
||||
return Icons[iconName as keyof typeof Icons];
|
||||
}
|
||||
export function AliasIcon(props: Icons.IconProps & { alias?: IconAlias }) {
|
||||
if (!props.alias) return null;
|
||||
|
||||
const iconName = toTitleCase(toCamelCase(props.alias));
|
||||
const Icon = Icons[iconName as keyof typeof Icons];
|
||||
return <Icon {...props} />;
|
||||
}
|
||||
@@ -61,7 +61,7 @@ type ListContainerProps = {
|
||||
context?: Context;
|
||||
refresh: () => void;
|
||||
header?: JSX.Element;
|
||||
placeholder: JSX.Element;
|
||||
placeholder: () => JSX.Element;
|
||||
isLoading?: boolean;
|
||||
button?: {
|
||||
onClick: () => void;
|
||||
@@ -136,7 +136,7 @@ function ListContainer(props: ListContainerProps) {
|
||||
<ListLoader />
|
||||
) : (
|
||||
<Flex variant="columnCenterFill" data-test-id="list-placeholder">
|
||||
{props.placeholder}
|
||||
<props.placeholder />
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
@@ -154,7 +154,7 @@ function ListContainer(props: ListContainerProps) {
|
||||
transition={{ duration: 0.2, delay: 0.1, ease: "easeInOut" }}
|
||||
ref={listContainerRef}
|
||||
variant="columnFill"
|
||||
data-test-id={`${type}-list`}
|
||||
data-test-id="note-list"
|
||||
>
|
||||
<Virtuoso
|
||||
ref={listRef}
|
||||
@@ -187,7 +187,6 @@ function ListContainer(props: ListContainerProps) {
|
||||
|
||||
switch (item.type) {
|
||||
case "header":
|
||||
if (!groupType) return null;
|
||||
return (
|
||||
<GroupHeader
|
||||
type={groupType}
|
||||
|
||||
@@ -28,7 +28,6 @@ import { db } from "../../common/db";
|
||||
import { getTotalNotes } from "../../common";
|
||||
import Reminder from "../reminder";
|
||||
import type { Reminder as ReminderType } from "@notesnook/core/collections/reminders";
|
||||
import { useMemo } from "react";
|
||||
|
||||
const SINGLE_LINE_HEIGHT = 1.4;
|
||||
const DEFAULT_LINE_HEIGHT =
|
||||
@@ -61,26 +60,19 @@ type ItemWrapper<TItem = Item> = (
|
||||
props: ItemWrapperProps<TItem>
|
||||
) => JSX.Element;
|
||||
|
||||
const NotesProfile: ItemWrapper = ({ index, item, type, context, compact }) => {
|
||||
const references = useMemo(
|
||||
() => getReferences(item.id, item.notebooks as Item[], context?.type),
|
||||
[item, context]
|
||||
);
|
||||
|
||||
return (
|
||||
<Note
|
||||
compact={compact}
|
||||
index={index}
|
||||
pinnable={!context}
|
||||
item={item}
|
||||
tags={getTags(item)}
|
||||
references={references}
|
||||
reminder={getReminder(item.id)}
|
||||
date={getDate(item, type)}
|
||||
context={context}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const NotesProfile: ItemWrapper = ({ index, item, type, context, compact }) => (
|
||||
<Note
|
||||
compact={compact}
|
||||
index={index}
|
||||
pinnable={!context}
|
||||
item={item}
|
||||
tags={getTags(item)}
|
||||
notebook={getNotebook(item.notebooks as Item[], context?.type)}
|
||||
reminder={getReminder(item.id)}
|
||||
date={getDate(item, type)}
|
||||
context={context}
|
||||
/>
|
||||
);
|
||||
|
||||
const NotebooksProfile: ItemWrapper = ({ index, item, type }) => (
|
||||
<Notebook
|
||||
@@ -137,36 +129,26 @@ function getTags(item: Item) {
|
||||
return tags || [];
|
||||
}
|
||||
|
||||
type Reference = {
|
||||
type: "topic" | "notebook";
|
||||
url: string;
|
||||
title: string;
|
||||
};
|
||||
type NotebookResult =
|
||||
| {
|
||||
id: string;
|
||||
title: string;
|
||||
dateEdited: number;
|
||||
topic: { id: string; title: string };
|
||||
}
|
||||
| undefined;
|
||||
|
||||
function getReferences(
|
||||
noteId: string,
|
||||
function getNotebook(
|
||||
notebooks: Item[],
|
||||
contextType?: string
|
||||
): { dateEdited: number; references: Reference[] } | undefined {
|
||||
if (["topic", "notebook"].includes(contextType || "")) return;
|
||||
): NotebookResult | undefined {
|
||||
if (contextType === "topic" || !notebooks?.length) return;
|
||||
|
||||
const references: Reference[] = [];
|
||||
let latestDateEdited = 0;
|
||||
|
||||
db.relations
|
||||
?.to({ id: noteId, type: "note" }, "notebook")
|
||||
?.forEach((notebook: any) => {
|
||||
references.push({
|
||||
type: "notebook",
|
||||
url: `/notebooks/${notebook.id}`,
|
||||
title: notebook.title
|
||||
} as Reference);
|
||||
|
||||
if (latestDateEdited < notebook.dateEdited)
|
||||
latestDateEdited = notebook.dateEdited;
|
||||
});
|
||||
|
||||
notebooks?.forEach((curr) => {
|
||||
return notebooks.reduce<NotebookResult>(function (
|
||||
prev: NotebookResult,
|
||||
curr
|
||||
): NotebookResult {
|
||||
if (prev) return prev;
|
||||
const topicId = (curr as NotebookReference).topics[0];
|
||||
const notebook = db.notebooks?.notebook(curr.id)?.data as NotebookType;
|
||||
if (!notebook) return;
|
||||
@@ -174,16 +156,14 @@ function getReferences(
|
||||
const topic = notebook.topics.find((t: Item) => t.id === topicId);
|
||||
if (!topic) return;
|
||||
|
||||
references.push({
|
||||
url: `/notebooks/${curr.id}/${topicId}`,
|
||||
title: topic.title,
|
||||
type: "topic"
|
||||
});
|
||||
if (latestDateEdited < (topic.dateEdited as number))
|
||||
latestDateEdited = topic.dateEdited as number;
|
||||
});
|
||||
|
||||
return { dateEdited: latestDateEdited, references: references.slice(0, 3) };
|
||||
return {
|
||||
id: notebook.id,
|
||||
title: notebook.title,
|
||||
dateEdited: notebook.dateEdited,
|
||||
topic: { id: topicId, title: topic.title }
|
||||
} as NotebookResult;
|
||||
},
|
||||
undefined as NotebookResult);
|
||||
}
|
||||
|
||||
function getReminder(noteId: string) {
|
||||
|
||||
@@ -138,7 +138,7 @@ function ListItem(props) {
|
||||
|
||||
backgroundColor: isSelected
|
||||
? "shade"
|
||||
: isMenuTarget || isFocused
|
||||
: isMenuTarget
|
||||
? "hover"
|
||||
: background,
|
||||
|
||||
@@ -167,11 +167,9 @@ function ListItem(props) {
|
||||
}}
|
||||
data-test-id={`list-item`}
|
||||
>
|
||||
{!isCompact && props.header}
|
||||
|
||||
<Text
|
||||
data-test-id={`title`}
|
||||
variant={isSimple || isCompact ? "body" : "subtitle"}
|
||||
variant={isSimple ? "body" : "subtitle"}
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
@@ -184,6 +182,8 @@ function ListItem(props) {
|
||||
{props.title}
|
||||
</Text>
|
||||
|
||||
{!isCompact && props.header}
|
||||
|
||||
{!isSimple && !isCompact && props.body && (
|
||||
<Text
|
||||
as="p"
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
function Note(props) {
|
||||
const {
|
||||
tags,
|
||||
references,
|
||||
notebook,
|
||||
item,
|
||||
index,
|
||||
context,
|
||||
@@ -115,16 +115,15 @@ function Note(props) {
|
||||
<Flex
|
||||
sx={{ alignItems: "center", flexWrap: "wrap", gap: 1, mt: "small" }}
|
||||
>
|
||||
{references?.references?.map((reference) => (
|
||||
{notebook && (
|
||||
<IconTag
|
||||
key={reference.url}
|
||||
onClick={() => {
|
||||
navigate(reference.url);
|
||||
navigate(`/notebooks/${notebook.id}/${notebook.topic.id}`);
|
||||
}}
|
||||
text={reference.title}
|
||||
icon={reference.type === "topic" ? Icon.Topic : Icon.Notebook}
|
||||
text={`${notebook.title} › ${notebook.topic.title}`}
|
||||
icon={Icon.Notebook}
|
||||
/>
|
||||
))}
|
||||
)}
|
||||
{reminder && isReminderActive(reminder) && (
|
||||
<IconTag
|
||||
icon={Icon.Reminder}
|
||||
@@ -261,7 +260,7 @@ export default React.memo(Note, function (prevProps, nextProps) {
|
||||
prevItem.conflicted === nextItem.conflicted &&
|
||||
prevItem.color === nextItem.color &&
|
||||
prevProps.compact === nextProps.compact &&
|
||||
prevProps.references?.dateEdited === nextProps.references?.dateEdited &&
|
||||
prevProps.notebook?.dateEdited === nextProps.notebook?.dateEdited &&
|
||||
prevProps.reminder?.dateModified === nextProps.reminder?.dateModified &&
|
||||
JSON.stringify(prevProps.tags) === JSON.stringify(nextProps.tags) &&
|
||||
JSON.stringify(prevProps.context) === JSON.stringify(nextProps.context)
|
||||
@@ -431,8 +430,8 @@ const menuItems = [
|
||||
await confirm({
|
||||
title: "Open duplicated note?",
|
||||
message: "Do you want to open the duplicated note?",
|
||||
negativeButtonText: "No",
|
||||
positiveButtonText: "Yes"
|
||||
noText: "No",
|
||||
yesText: "Yes"
|
||||
})
|
||||
) {
|
||||
hashNavigate(`/notes/${id}/edit`, { replace: true });
|
||||
@@ -455,14 +454,40 @@ const menuItems = [
|
||||
title: "Prevent this item from syncing?",
|
||||
message:
|
||||
"Turning sync off for this item will automatically delete it from all other devices & any future changes to this item won't get synced. Are you sure you want to continue?",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
yesText: "Yes",
|
||||
noText: "No"
|
||||
}))
|
||||
)
|
||||
await store.get().localOnly(note.id);
|
||||
}
|
||||
},
|
||||
{ key: "sep3", type: "separator" },
|
||||
{
|
||||
key: "removefromtopic",
|
||||
title: "Remove from topic",
|
||||
icon: Icon.TopicRemove,
|
||||
hidden: ({ context }) => context?.type !== "topic",
|
||||
onClick: async ({ items, context }) => {
|
||||
try {
|
||||
if (!context.value?.topic || !context.value?.id)
|
||||
throw new Error("context is missing");
|
||||
|
||||
const ids = items.map((i) => i.id);
|
||||
|
||||
await db.notes.removeFromNotebook(
|
||||
{ id: context.value.id, topic: context.value.topic },
|
||||
...ids
|
||||
);
|
||||
|
||||
store.refresh();
|
||||
|
||||
showToast("success", "Note removed from topic.");
|
||||
} catch (e) {
|
||||
showToast("error", `Failed to remove note from topic: ${e.message}.`);
|
||||
}
|
||||
},
|
||||
multiSelect: true
|
||||
},
|
||||
{
|
||||
key: "movetotrash",
|
||||
title: "Move to trash",
|
||||
@@ -507,63 +532,40 @@ function colorsToMenuItems() {
|
||||
});
|
||||
}
|
||||
|
||||
function notebooksMenuItems({ items }) {
|
||||
const noteIds = items.map((i) => i.id);
|
||||
|
||||
function notebooksMenuItems({ note }) {
|
||||
const menuItems = [];
|
||||
menuItems.push({
|
||||
key: "link-notebooks",
|
||||
title: "Link to...",
|
||||
icon: Icon.AddToNotebook,
|
||||
onClick: async () => {
|
||||
await showMoveNoteDialog(noteIds);
|
||||
onClick: async ({ items }) => {
|
||||
await showMoveNoteDialog(items.map((i) => i.id));
|
||||
}
|
||||
});
|
||||
|
||||
const notebooks = items
|
||||
.map((note) => db.relations?.to(note, "notebook"))
|
||||
.flat();
|
||||
const topics = items.map((note) => note.notebooks || []).flat();
|
||||
|
||||
if (topics?.length > 0 || notebooks?.length > 0) {
|
||||
if (note && note.notebooks?.length > 0) {
|
||||
menuItems.push(
|
||||
{
|
||||
key: "remove-from-all-notebooks",
|
||||
title: "Unlink from all",
|
||||
icon: Icon.RemoveShortcutLink,
|
||||
onClick: async () => {
|
||||
await db.notes.removeFromAllNotebooks(...noteIds);
|
||||
await db.notes.removeFromAllNotebooks(note.id);
|
||||
store.refresh();
|
||||
}
|
||||
},
|
||||
{ key: "sep", type: "separator" }
|
||||
);
|
||||
|
||||
notebooks?.forEach((notebook) => {
|
||||
if (menuItems.find((item) => item.key === notebook.id)) return;
|
||||
|
||||
menuItems.push({
|
||||
key: notebook.id,
|
||||
title: notebook.title,
|
||||
icon: Icon.Notebook,
|
||||
checked: true,
|
||||
tooltip: "Click to remove from this notebook",
|
||||
onClick: async () => {
|
||||
await db.notes.removeFromNotebook({ id: notebook.id }, ...noteIds);
|
||||
store.refresh();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
topics?.forEach((ref) => {
|
||||
note.notebooks.forEach((ref) => {
|
||||
// if (prev) return prev;
|
||||
// const topicId = (curr as NotebookReference).topics[0];
|
||||
const notebook = db.notebooks?.notebook(ref.id);
|
||||
if (!notebook) return;
|
||||
const notebookMenuItems = [];
|
||||
for (const topicId of ref.topics) {
|
||||
if (!notebook.topics.topic(topicId)) continue;
|
||||
if (menuItems.find((item) => item.key === topicId)) continue;
|
||||
|
||||
const topic = notebook.topics.topic(topicId)._topic;
|
||||
menuItems.push({
|
||||
notebookMenuItems.push({
|
||||
key: topicId,
|
||||
title: topic.title,
|
||||
icon: Icon.Topic,
|
||||
@@ -572,12 +574,19 @@ function notebooksMenuItems({ items }) {
|
||||
onClick: async () => {
|
||||
await db.notes.removeFromNotebook(
|
||||
{ id: ref.id, topic: topic.id },
|
||||
...noteIds
|
||||
note.id
|
||||
);
|
||||
store.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
key: ref.id,
|
||||
title: notebook.title,
|
||||
icon: Icon.Notebook2,
|
||||
items: notebookMenuItems
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -155,33 +155,25 @@ const menuItems = [
|
||||
iconColor: "error",
|
||||
icon: Icon.Trash,
|
||||
onClick: async ({ items }) => {
|
||||
const result = await confirm({
|
||||
title: `Delete ${pluralize(items.length, "notebook", "notebooks")}?`,
|
||||
positiveButtonText: `Yes`,
|
||||
negativeButtonText: "No",
|
||||
checks: {
|
||||
deleteContainingNotes: {
|
||||
text: `Move all notes in ${
|
||||
items.length > 1 ? "these notebooks" : "this notebook"
|
||||
} to trash`
|
||||
}
|
||||
}
|
||||
const phrase = items.length > 1 ? "this notebook" : "these notebooks";
|
||||
const shouldDeleteNotes = await confirm({
|
||||
title: `Delete notes in ${phrase}?`,
|
||||
message: `These notes will be moved to trash and permanently deleted after 7 days.`,
|
||||
yesText: `Yes`,
|
||||
noText: "No"
|
||||
});
|
||||
|
||||
if (result) {
|
||||
if (result.deleteContainingNotes) {
|
||||
const notes = [];
|
||||
for (const item of items) {
|
||||
notes.push(...db.relations.from(item, "note"));
|
||||
const topics = db.notebooks.notebook(item.id).topics;
|
||||
for (const topic of topics.all) {
|
||||
notes.push(...topics.topic(topic.id).all);
|
||||
}
|
||||
if (shouldDeleteNotes) {
|
||||
const notes = [];
|
||||
for (const item of items) {
|
||||
const topics = db.notebooks.notebook(item.id).topics;
|
||||
for (const topic of topics.all) {
|
||||
notes.push(...topics.topic(topic.id).all);
|
||||
}
|
||||
await Multiselect.moveNotesToTrash(notes, false);
|
||||
}
|
||||
await Multiselect.moveNotebooksToTrash(items);
|
||||
await Multiselect.moveNotesToTrash(notes, false);
|
||||
}
|
||||
await Multiselect.moveNotebooksToTrash(items);
|
||||
},
|
||||
multiSelect: true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
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 Placeholder from "./index";
|
||||
|
||||
function AttachmentsPlaceholder() {
|
||||
return (
|
||||
<Placeholder
|
||||
id="attachments"
|
||||
title="Your attachments"
|
||||
text="You haven't attached any files yet."
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default AttachmentsPlaceholder;
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
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 Placeholder from "./index";
|
||||
|
||||
function FavoritesPlaceholder() {
|
||||
return (
|
||||
<Placeholder
|
||||
id="favorites"
|
||||
title="Your favorites"
|
||||
text="Notes you favorite will appear here."
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default FavoritesPlaceholder;
|
||||
63
apps/web/src/components/placeholders/index.js
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
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 { Button, Flex, Text } from "@theme-ui/components";
|
||||
|
||||
const PlaceholderLoader = React.lazy(() => import("./loader"));
|
||||
function Placeholder(props) {
|
||||
const { id, text, callToAction } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
variant="columnCenter"
|
||||
sx={{ position: "relative", alignSelf: "stretch" }}
|
||||
>
|
||||
<React.Suspense fallback={<div />}>
|
||||
<PlaceholderLoader name={id} width={"150px"} height={"150px"} />
|
||||
</React.Suspense>
|
||||
<Text
|
||||
variant="body"
|
||||
mt={2}
|
||||
mx={4}
|
||||
sx={{ textAlign: "center", color: "fontTertiary" }}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
{callToAction && (
|
||||
<Button
|
||||
mt={1}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
display: "flex"
|
||||
}}
|
||||
variant="tool"
|
||||
onClick={callToAction.onClick}
|
||||
>
|
||||
<callToAction.icon size={18} color="primary" />
|
||||
<Text ml={1}>{callToAction.text}</Text>
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default Placeholder;
|
||||
@@ -1,81 +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 { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { Context, useTip } from "../../hooks/use-tip";
|
||||
import { Info } from "../icons";
|
||||
import { AliasIcon } from "../icons/resolver";
|
||||
|
||||
type PlaceholderProps = { context: Context; text?: string };
|
||||
function Placeholder(props: PlaceholderProps) {
|
||||
const { context, text } = props;
|
||||
const tip = useTip(context);
|
||||
|
||||
if (!tip) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
variant="columnCenter"
|
||||
sx={{
|
||||
position: "relative",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "flex-start",
|
||||
alignSelf: "stretch",
|
||||
px: 6
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
border: "1px solid var(--primary)",
|
||||
borderRadius: 50,
|
||||
p: 1,
|
||||
py: "1.5px"
|
||||
}}
|
||||
>
|
||||
<Info color="primary" size={13} sx={{ mr: "small" }} />
|
||||
<Text variant="subBody" sx={{ fontSize: 10 }} color="primary">
|
||||
TIP
|
||||
</Text>
|
||||
</Flex>
|
||||
<Text variant="body" sx={{ color: "icon", mt: 1 }}>
|
||||
{text || tip.text}
|
||||
</Text>
|
||||
{tip.button && (
|
||||
<Button
|
||||
sx={{
|
||||
mt: 2,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
display: "flex"
|
||||
}}
|
||||
variant="tool"
|
||||
onClick={tip.button.onClick}
|
||||
>
|
||||
<Text mr={1} color="primary">
|
||||
{tip.button.title}
|
||||
</Text>
|
||||
<AliasIcon alias={tip.button.icon} size={18} color="primary" />
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default Placeholder;
|
||||
52
apps/web/src/components/placeholders/loader.js
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
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 { useLayoutEffect } from "react";
|
||||
import { ReactComponent as Note } from "../../assets/note2.svg";
|
||||
import { ReactComponent as Notebook } from "../../assets/notebook.svg";
|
||||
import { ReactComponent as Monographs } from "../../assets/monographs.svg";
|
||||
import { ReactComponent as Search } from "../../assets/search.svg";
|
||||
import { ReactComponent as Tag } from "../../assets/tag.svg";
|
||||
import { ReactComponent as Trash } from "../../assets/trash.svg";
|
||||
import { ReactComponent as Fav } from "../../assets/fav.svg";
|
||||
import { ReactComponent as Attachment } from "../../assets/attachment.svg";
|
||||
import { ReactComponent as Reminder } from "../../assets/reminder.svg";
|
||||
|
||||
const Placeholders = {
|
||||
note: Note,
|
||||
notebook: Notebook,
|
||||
topic: Notebook,
|
||||
monograph: Monographs,
|
||||
search: Search,
|
||||
tag: Tag,
|
||||
trash: Trash,
|
||||
favorites: Fav,
|
||||
attachments: Attachment,
|
||||
reminder: Reminder
|
||||
};
|
||||
|
||||
export default function PlaceholderLoader({ name, onLoad, ...restProps }) {
|
||||
const Component = Placeholders[name];
|
||||
useLayoutEffect(() => {
|
||||
onLoad && onLoad();
|
||||
}, [onLoad]);
|
||||
|
||||
if (!Component) return null;
|
||||
return <Component {...restProps} />;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
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 Placeholder from "./index";
|
||||
|
||||
function MonographsPlaceholder() {
|
||||
return (
|
||||
<Placeholder
|
||||
id="monographs"
|
||||
title="Your monographs"
|
||||
text="All your published notes will be shown here."
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default MonographsPlaceholder;
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
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 Placeholder from "./index";
|
||||
import { Plus } from "../icons";
|
||||
import { hashNavigate } from "../../navigation";
|
||||
|
||||
function NotebooksPlaceholder() {
|
||||
return (
|
||||
<Placeholder
|
||||
id="notebook"
|
||||
title="Your notebooks"
|
||||
text="You have not made any notebooks yet."
|
||||
callToAction={{
|
||||
text: "Make your first notebook",
|
||||
icon: Plus,
|
||||
onClick: () => hashNavigate("/notebooks/create")
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default NotebooksPlaceholder;
|
||||
39
apps/web/src/components/placeholders/notesplacholder.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
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 Placeholder from "./index";
|
||||
import * as Icon from "../icons";
|
||||
import { hashNavigate } from "../../navigation";
|
||||
|
||||
function NotesPlaceholder() {
|
||||
return (
|
||||
<Placeholder
|
||||
id="note"
|
||||
title="Your notes"
|
||||
text="You have not made any notes yet."
|
||||
callToAction={{
|
||||
text: "Make your first note",
|
||||
icon: Icon.Plus,
|
||||
onClick: () =>
|
||||
hashNavigate("/notes/create", { replace: true, addNonce: true })
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default NotesPlaceholder;
|
||||