Files
notesnook/apps/mobile/app/components/note-history/index.tsx
2026-08-14 12:34:07 +05:00

266 lines
7.7 KiB
TypeScript

/*
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, { RefObject, useCallback, useEffect, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native";
import { LegendList } from "@legendapp/list";
import { getFormattedDate, getTimeAgo } from "@notesnook/common";
import { HistorySession, Note, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useDBItem } from "../../hooks/use-db-item";
import { presentSheet, ToastManager } from "../../services/event-manager";
import { openLinkInBrowser } from "../../utils/functions";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import DialogHeader from "../dialog/dialog-header";
import SheetProvider from "../sheet-provider";
import { Pressable } from "../ui/pressable";
import Seperator from "../ui/seperator";
import Paragraph from "../ui/typography/paragraph";
import NotePreview from "./preview";
import { presentDialog } from "../dialog/functions";
import { Dialog } from "../dialog";
const HistoryItem = ({
index,
items,
note,
refresh
}: {
index: number;
items?: VirtualizedGrouping<HistorySession>;
note: Note;
refresh: () => void;
}) => {
const [item] = useDBItem(index, "noteHistory", items);
const { colors } = useThemeColors();
const getDate = (start: number, end: number) => {
const _start_date = getFormattedDate(start, "date");
const _end_date = getFormattedDate(end + 60000, "date");
const _start_time = getFormattedDate(start, "time");
const _end_time = getFormattedDate(end + 60000, "time");
return `${_start_date} ${_start_time} - ${
_end_date === _start_date ? " " : _end_date + " "
}${_end_time}`;
};
const preview = useCallback(
async (item: HistorySession) => {
const content = await db.noteHistory.content(item.id);
presentSheet({
component: (
<NotePreview
session={{
...item,
session: getDate(item.dateCreated, item.dateModified)
}}
content={content}
note={note}
update={refresh}
/>
),
context: "note_history"
});
},
[note, refresh]
);
return (
<Pressable
type="secondary"
onPress={() => {
if (!item) return;
preview(item);
}}
style={{
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
height: 45,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
flexDirection: "row"
}}
>
{!item ? null : (
<>
<Paragraph>{getDate(item.dateCreated, item.dateModified)}</Paragraph>
<Paragraph color={colors.secondary.paragraph} size={AppFontSize.xs}>
{getTimeAgo(item.dateModified)}
</Paragraph>
</>
)}
</Pressable>
);
};
export default function NoteHistory({
note,
fwdRef
}: {
note: Note;
fwdRef: RefObject<ActionSheetRef>;
}) {
const [history, setHistory] = useState<VirtualizedGrouping<HistorySession>>();
const [_loading, setLoading] = useState(true);
const { colors } = useThemeColors();
const refresh = useCallback(() => {
db.noteHistory
.get(note.id)
.sorted({
sortBy: "dateModified",
sortDirection: "desc"
})
.then((result) => {
setHistory(result);
setLoading(false);
})
.catch((e) => {
setLoading(false);
});
}, [note.id]);
useEffect(() => {
refresh();
}, [refresh]);
const renderItem = useCallback(
({ index }: { index: number }) => (
<HistoryItem
index={index}
items={history}
note={note}
refresh={refresh}
/>
),
[history, note, refresh]
);
return (
<View>
<SheetProvider context="note_history" />
<Dialog context="local" />
<DialogHeader
title={strings.noteHistory()}
padding={12}
button={
history?.placeholders.length
? {
title: strings.clearHistory(),
type: "secondary",
onPress: () => {
presentDialog({
title: strings.clearHistory(),
paragraph: strings.clearHistoryConfirmation(),
positiveText: strings.clear(),
negativeText: strings.cancel(),
positiveType: "errorShade",
context: "local",
positivePress: async () => {
await db.noteHistory.clearSessions(note.id);
fwdRef.current?.hide();
ToastManager.show({
heading: strings.historyCleared(),
type: "success"
});
}
});
}
}
: undefined
}
/>
<Seperator />
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
height: !history?.placeholders.length
? 300
: (history.placeholders.length + 1) * 55,
maxHeight: "100%"
}}
>
<LegendList
renderScrollComponent={(props) => <ScrollView {...props} />}
data={history?.placeholders || []}
estimatedItemSize={55}
extraData={history}
ListEmptyComponent={
<View
style={{
width: "100%",
justifyContent: "center",
alignItems: "center",
height: 300,
gap: 10
}}
>
{_loading ? (
<ActivityIndicator
size={AppFontSize.xl}
color={colors.primary.accent}
/>
) : (
<>
<Icon name="history" size={50} color={colors.primary.icon} />
<Paragraph color={colors.secondary.paragraph}>
{strings.noteHistoryPlaceholder()}
</Paragraph>
</>
)}
</View>
}
renderItem={renderItem}
/>
</View>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
style={{
alignSelf: "center"
}}
>
{strings.noteHistoryNotice[0]()}{" "}
<Text
onPress={() => {
openLinkInBrowser(
"https://notesnook.com/help/note-version-history"
);
}}
style={{
color: colors.primary.accent,
textDecorationLine: "underline"
}}
>
{strings.noteHistoryNotice[1]()}
</Text>
</Paragraph>
</View>
);
}