Compare commits

..

4 Commits

Author SHA1 Message Date
Ammar Ahmed
2febd09613 Merge branch 'master' into fix/sync-quick-notes
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2023-04-26 15:13:27 +05:00
ammarahm-ed
91138f4911 mobile: ensure full db init when sync is required 2023-04-26 15:12:29 +05:00
Ammar Ahmed
8a138d960d Update apps/mobile/app/services/notifications.ts
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2023-04-26 14:57:06 +05:00
ammarahm-ed
59442b9107 mobile: sync quick notes 2023-04-26 09:51:24 +05:00
315 changed files with 89442 additions and 22662 deletions

View File

@@ -20,7 +20,6 @@ const SCOPES = [
"logger",
"theme",
"core",
"fs",
"clipper",
"config",
"ci",

View File

@@ -31,6 +31,14 @@ import { TipManager } from "./services/tip-manager";
import { useUserStore } from "./stores/use-user-store";
import { View } from "react-native";
import { useState } from "react";
import NetInfo from "@react-native-community/netinfo";
NetInfo.configure({
reachabilityUrl: "https://notesnook.com",
reachabilityTest: (response) => {
if (!response) return false;
return response?.status >= 200 && response?.status < 300;
}
});
SettingsService.init();
SettingsService.checkOrientation();

View File

@@ -22,17 +22,10 @@ import "react-native-get-random-values";
import * as Keychain from "react-native-keychain";
import { generateSecureRandom } from "react-native-securerandom";
import Sodium from "@ammarahmed/react-native-sodium";
import { MMKV } from "./mmkv";
const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
const IOS_KEYCHAIN_UPGRAGE_KEY = "keychain-ios:upgraded";
const KEYSTORE_CONFIG = Platform.select({
ios: {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
accessGroup: IOS_KEYCHAIN_ACCESS_GROUP,
service: IOS_KEYCHAIN_SERVICE_NAME
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY
},
android: {}
});
@@ -46,27 +39,12 @@ export async function deriveCryptoKey(name, data) {
credentials.key,
KEYSTORE_CONFIG
);
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
return credentials.key;
} catch (e) {
console.error(e);
}
}
async function upgradeIOSKeychain(username, password) {
if (Platform.OS !== "ios") return;
if (!MMKV.getBool(IOS_KEYCHAIN_UPGRAGE_KEY)) {
await Keychain.setInternetCredentials(
"notesnook",
username,
password,
KEYSTORE_CONFIG
);
console.log("IOS KEYCHAIN MIGRATION COMPLETED!");
MMKV.setBool(IOS_KEYCHAIN_UPGRAGE_KEY, true);
}
}
export async function getCryptoKey(_name) {
try {
if (await Keychain.hasInternetCredentials("notesnook")) {
@@ -74,9 +52,6 @@ export async function getCryptoKey(_name) {
"notesnook",
KEYSTORE_CONFIG
);
// upgrades ios keychain to use accessGroups
// so we have access to keychain in share extension.
await upgradeIOSKeychain(credentials.username, credentials.password);
return credentials.password;
} else {
return null;

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
import {
decrypt,
deriveCryptoKey,

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Dimensions, Platform } from "react-native";
import ImageResizer from "@bam.tech/react-native-image-resizer";
import RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
/**
* Scale down & compress images to screen width
* for loading in editor.

View File

@@ -17,169 +17,25 @@ 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 Sodium from "@ammarahmed/react-native-sodium";
import {
getFileNameWithExtension,
isImage,
isDocument
} from "@notesnook/core/utils/filename";
import React from "react";
import { Platform } from "react-native";
import * as ScopedStorage from "react-native-scoped-storage";
import { subscribe, zip } from "react-native-zip-archive";
import RNFetchBlob from "react-native-blob-util";
import Sodium from "@ammarahmed/react-native-sodium";
import RNFetchBlob from "rn-fetch-blob";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { ToastEvent, presentSheet } from "../../services/event-manager";
import { presentSheet, ToastEvent } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import Storage from "../database/storage";
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
export const FileDownloadStatus = {
Success: 1,
Fail: 0
};
/**
* Download all user's attachments
* @returns
*/
export function downloadAllAttachments() {
const attachments = db.attachments.all;
return downloadAttachments(attachments);
}
/**
* Downloads provided attachments to a .zip file
* on user's device.
* @param attachments
* @param onProgress
* @returns
*/
export async function downloadAttachments(
attachments,
onProgress,
canceled,
groupId
) {
if (!attachments || !attachments.length) return;
const result = new Map();
let outputFolder;
if (Platform.OS === "android") {
// Ask the user to select a directory to store the file
let file = await ScopedStorage.openDocumentTree(true);
outputFolder = file.uri;
if (!outputFolder) return;
} else {
outputFolder = await Storage.checkAndCreateDir("/downloads/");
}
// Create the folder to zip;
const zipSourceFolder = `${cacheDir}/notesnook-attachments`;
const zipOutputFile =
Platform.OS === "ios"
? `${outputFolder}/notesnook-attachments-${Date.now()}.zip`
: `${cacheDir}/notesnook-attachments.zip`;
if (await RNFetchBlob.fs.exists(zipSourceFolder))
await RNFetchBlob.fs.unlink(zipSourceFolder);
await RNFetchBlob.fs.mkdir(zipSourceFolder);
for (let i = 0; i < attachments.length; i++) {
let attachment = attachments[i];
const hash = attachment.metadata.hash;
try {
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
onProgress?.(
i + 1 / attachments.length,
`Downloading attachments (${i + 1}/${
attachments.length
})... Please wait`
);
// Download to cache
let uri = await downloadAttachment(hash, false, {
silent: true,
cache: true,
groupId: groupId
});
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
if (!uri) throw new Error("Failed to download file");
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
const filePath = `${zipSourceFolder}/${attachment.metadata.filename}`;
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
result.set(hash, {
filename: attachment.metadata.filename,
status: FileDownloadStatus.Success
});
} catch (e) {
result.set(hash, {
filename: attachment.metadata.filename,
status: FileDownloadStatus.Fail,
reason: e
});
console.log("Error downloading attachment", e);
}
}
if (canceled.current) {
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
return;
}
if (result?.size) {
let sub;
try {
onProgress?.(0, `Zipping... Please wait`);
// If all goes well, zip the notesnook-attachments folder in cache.
sub = subscribe(({ progress }) => {
onProgress(
progress,
`Saving zip file (${(progress * 100).toFixed(1)}%)... Please wait`
);
});
await zip(zipSourceFolder, zipOutputFile);
sub?.remove();
onProgress(1, `Saving zip file... Please wait`);
if (Platform.OS === "android") {
// Move the zip to user selected directory.
const file = await ScopedStorage.createFile(
outputFolder,
`notesnook-attachments-${Date.now()}.zip`,
"application/zip"
);
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
}
onProgress?.(1, `Done`);
releasePermissions(outputFolder);
} catch (e) {
releasePermissions(outputFolder);
sub?.remove();
console.log("Error zipping attachments", e);
}
// Remove source & zip file from cache.
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
if (Platform.OS === "android") {
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
}
}
return result;
}
import { cacheDir } from "./utils";
import { getFileNameWithExtension } from "@notesnook/core/utils/filename";
export default async function downloadAttachment(
hash,
global = true,
options = {
silent: false,
cache: false,
throwError: false,
groupId: undefined
cache: false
}
) {
let attachment = db.attachments.attachment(hash);
@@ -199,13 +55,8 @@ export default async function downloadAttachment(
}
try {
console.log(
"starting download attachment",
attachment.metadata.hash,
options.groupId
);
await db.fs.downloadFile(
options.groupId || attachment.metadata.hash,
attachment.metadata.hash,
attachment.metadata.hash
);
if (
@@ -248,8 +99,7 @@ export default async function downloadAttachment(
if (
attachment.dateUploaded &&
!isImage(attachment.metadata?.type) &&
!isDocument(attachment.metadata?.type)
!attachment.metadata?.type?.startsWith("image")
) {
RNFetchBlob.fs
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.metadata.hash}`)
@@ -281,8 +131,5 @@ export default async function downloadAttachment(
.catch(console.log);
}
useAttachmentStore.getState().remove(attachment.metadata.hash);
if (options.throwError) {
throw e;
}
}
}

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import hosts from "@notesnook/core/utils/constants";
import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
import { ToastEvent } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";

View File

@@ -19,29 +19,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Platform } from "react-native";
import Sodium from "@ammarahmed/react-native-sodium";
import RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
import { cacheDir, getRandomId } from "./utils";
import { db } from "../database";
import { compressToBase64 } from "./compress";
import { IOS_APPGROUPID } from "../../utils/constants";
export async function readEncrypted(filename, key, cipherData) {
let path = `${cacheDir}/${filename}`;
try {
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists =
(await RNFetchBlob.fs.exists(path)) ||
(Platform.OS === "ios" && (await RNFetchBlob.fs.exists(appGroupPath)));
let exists = await RNFetchBlob.fs.exists(path);
if (!exists) {
return false;
}
const attachment = db.attachments.attachment(filename);
const isPng = /(png)/g.test(attachment?.metadata.type);
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
@@ -50,8 +39,7 @@ export async function readEncrypted(filename, key, cipherData) {
key,
{
...cipherData,
hash: filename,
appGroupId: IOS_APPGROUPID
hash: filename
},
cipherData.outputType === "base64"
? isPng || isJpeg

View File

@@ -17,13 +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 RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import { cacheDir } from "./utils";
import { isImage, isDocument } from "@notesnook/core/utils/filename";
import { Platform } from "react-native";
import { IOS_APPGROUPID } from "../../utils/constants";
export async function uploadFile(filename, data, cancelToken) {
if (!data) return false;
@@ -38,19 +35,9 @@ export async function uploadFile(filename, data, cancelToken) {
if (!res.ok) throw new Error(`${res.status}: Unable to resolve upload url`);
const uploadUrl = await res.text();
if (!uploadUrl) throw new Error("Unable to resolve upload url");
let uploadFilePath = `${cacheDir}/${filename}`;
const iosAppGroup =
Platform.OS === "ios"
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
: null;
const appGroupPath = `${iosAppGroup}/${filename}`;
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
if (!exists && Platform.OS === "ios") {
uploadFilePath = appGroupPath;
}
let request = RNFetchBlob.config({
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
IOSBackgroundTask: true
})
.fetch(
"PUT",
@@ -58,7 +45,7 @@ export async function uploadFile(filename, data, cancelToken) {
{
"content-type": ""
},
RNFetchBlob.wrap(uploadFilePath)
RNFetchBlob.wrap(`${cacheDir}/${filename}`)
)
.uploadProgress((sent, total) => {
useAttachmentStore
@@ -76,10 +63,7 @@ export async function uploadFile(filename, data, cancelToken) {
if (result) {
let attachment = db.attachments.attachment(filename);
if (!attachment) return result;
if (
!isImage(attachment.metadata.type) &&
!isDocument(attachment.metadata?.type)
) {
if (!attachment.metadata.type.startsWith("image/")) {
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
}
}

View File

@@ -17,9 +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 * as ScopedStorage from "react-native-scoped-storage";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
export const cacheDir = RNFetchBlob.fs.dirs.CacheDir;
@@ -64,22 +62,3 @@ export function cancelable(operation) {
};
};
}
export function copyFileAsync(source, dest) {
return new Promise((resolve) => {
ScopedStorage.copyFile(source, dest, (e, r) => {
console.log(e, r);
resolve();
});
});
}
export async function releasePermissions(path) {
if (Platform.OS === "ios") return;
const uris = await ScopedStorage.getPersistedUriPermissions();
for (let uri of uris) {
if (path.startsWith(uri)) {
await ScopedStorage.releasePersistableUriPermission(uri);
}
}
}

View File

@@ -148,7 +148,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
<Button
key={item.title}
title={item.title}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
type="gray"
onPress={() => onPress(item)}
width={null}

View File

@@ -67,7 +67,7 @@ export const Title = ({ text, style = {}, inline }) => {
right: 0
}}
iconSize={24}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
style={{
borderRadius: 100,
paddingVertical: 0,

View File

@@ -102,21 +102,15 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
if (res.failed) {
db.attachments.markAsFailed(attachment.id, res.failed);
setFailed(res.failed);
ToastEvent.show({
heading: "File check failed with error: " + res.failed,
type: "error",
context: "local"
});
} else {
setFailed(null);
db.attachments.markAsFailed(attachment.id, null);
ToastEvent.show({
heading: "File check passed",
type: "success",
context: "local"
});
}
ToastEvent.show({
heading: "File check passed",
type: "success",
context: "local"
});
setAttachments([...db.attachments.all]);
setLoading({
name: null
@@ -215,7 +209,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
}}
>
<Paragraph
size={SIZE.xs}
size={SIZE.xs + 1}
style={{
marginRight: 10
}}
@@ -227,7 +221,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
style={{
marginRight: 10
}}
size={SIZE.xs}
size={SIZE.xs + 1}
color={colors.icon}
>
{formatBytes(attachment.length)}
@@ -238,7 +232,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
style={{
marginRight: 10
}}
size={SIZE.xs}
size={SIZE.xs + 1}
color={colors.icon}
>
{attachment.noteIds.length} note
@@ -254,7 +248,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
context: "local"
});
}}
size={SIZE.xs}
size={SIZE.xs + 1}
color={colors.icon}
>
{attachment.metadata.hash}
@@ -310,7 +304,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
}}
key={item.id}
>
<Paragraph size={SIZE.xs}>{item.title}</Paragraph>
<Paragraph size={SIZE.xs + 1}>{item.title}</Paragraph>
</PressableButton>
))}
</>

View File

@@ -22,6 +22,7 @@ import { TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { formatBytes } from "../../utils";
import { SIZE } from "../../utils/size";
@@ -35,27 +36,24 @@ function getFileExtension(filename) {
var ext = /^.+\.([^.]+)$/.exec(filename);
return ext == null ? "" : ext[1];
}
export const AttachmentItem = ({
attachment,
encryption,
setAttachments,
pressable = true,
hideWhenNotDownloading
}) => {
/**
*
* @param {any} param0
* @returns
*/
export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
const colors = useThemeStore((state) => state.colors);
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
attachment,
encryption
);
const encryptionProgress = useAttachmentStore(
(state) => state.encryptionProgress
);
const onPress = () => {
if (!pressable) return;
Actions.present(attachment, setAttachments, attachment.metadata.hash);
};
return hideWhenNotDownloading &&
(!currentProgress || !currentProgress.value) ? null : (
return (
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
@@ -118,22 +116,22 @@ export const AttachmentItem = ({
{attachment.metadata.filename}
</Paragraph>
{!hideWhenNotDownloading ? (
<Paragraph color={colors.icon} size={SIZE.xs}>
{formatBytes(attachment.length)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
</Paragraph>
) : null}
<Paragraph color={colors.icon} size={SIZE.xs}>
{formatBytes(attachment.length)}{" "}
{currentProgress?.type
? "(" + currentProgress.type + "ing - tap to cancel)"
: ""}
</Paragraph>
</View>
</View>
{currentProgress ? (
{currentProgress ||
(encryptionProgress && encryptionProgress !== "0.00") ||
encryption ? (
<TouchableOpacity
activeOpacity={0.9}
onPress={() => {
if (encryption || !pressable) return;
if (encryption) return;
db.fs.cancel(attachment.metadata.hash);
setCurrentProgress(null);
}}
@@ -146,7 +144,13 @@ export const AttachmentItem = ({
>
<ProgressCircleComponent
size={SIZE.xxl}
progress={currentProgress?.value ? currentProgress?.value / 100 : 0}
progress={
encryptionProgress
? encryptionProgress
: currentProgress?.value
? currentProgress?.value / 100
: 0
}
showsText
textStyle={{
fontSize: 10

View File

@@ -1,273 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useRef, useState } from "react";
import { Platform, View } from "react-native";
import { db } from "../../common/database";
import { downloadAttachments } from "../../common/filesystem/download-attachment";
import { presentSheet } from "../../services/event-manager";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { ProgressBarComponent } from "../ui/svg/lazy";
import { useThemeStore } from "../../stores/use-theme-store";
import { FlatList } from "react-native-actions-sheet";
import { AttachmentItem } from "./attachment-item";
const DownloadAttachments = ({ close, attachments, isNote, update }) => {
const colors = useThemeStore((state) => state.colors);
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState({
value: 0,
statusText: "Download started... Please wait"
});
const [result, setResult] = useState(new Map());
const canceled = useRef(false);
const groupId = useRef();
const onDownload = async () => {
update({
disableClosing: true
});
setDownloading(true);
canceled.current = false;
groupId.current = Date.now().toString();
const result = await downloadAttachments(
attachments,
(progress, statusText) => setProgress({ value: progress, statusText }),
canceled,
groupId.current
);
if (canceled.current) return;
setResult(result || new Map());
setDownloading(false);
update({
disableClosing: false
});
};
const cancel = async () => {
update({
disableClosing: false
});
canceled.current = true;
console.log(groupId.current, "canceling groupId downloads");
await db.fs.cancel(groupId.current);
setDownloading(false);
groupId.current = null;
};
const successResults = () => {
const results = [];
for (let [key, value] of result.entries()) {
if (value.status === 1) results.push(db.attachments.attachment(key));
}
return results;
};
const failedResults = () => {
const results = [];
for (let [key, value] of result.entries()) {
if (value.status === 0) results.push(db.attachments.attachment(key));
}
return results;
};
function getResultText() {
const downloadedAttachmentsCount =
attachments.length - failedResults().length;
if (downloadedAttachmentsCount === 0)
return "Failed to download all attachments";
return `Successfully downloaded ${downloadedAttachmentsCount}/${
attachments.length
} attachments as a zip file at ${
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
}`;
}
return (
<View
style={{
alignItems: "center",
width: "100%",
paddingVertical: 12,
paddingHorizontal: 12
}}
>
<Heading>
{downloading
? "Downloading attachments"
: result?.size
? "Downloaded attachments"
: "Download attachments"}
</Heading>
{downloading ? (
<Paragraph
style={{
textAlign: "center"
}}
>
{progress.statusText}
</Paragraph>
) : result?.size ? (
<Paragraph
style={{
textAlign: "center"
}}
>
{getResultText()}
</Paragraph>
) : (
<Paragraph
style={{
textAlign: "center"
}}
>
Are you sure you want to download all attachments
{isNote ? " of this note?" : "?"}
</Paragraph>
)}
{downloading ? (
<View
style={{
width: 200,
marginTop: 10
}}
>
<ProgressBarComponent
height={5}
width={null}
animated={true}
useNativeDriver
progress={progress.value ? progress.value / attachments.length : 0}
unfilledColor={colors.nav}
color={colors.accent}
borderWidth={0}
/>
</View>
) : null}
<FlatList
style={{
maxHeight: 300,
width: "100%",
minHeight: 60,
backgroundColor: colors.nav,
borderRadius: 5,
marginVertical: 12
}}
data={downloading ? attachments : []}
ListEmptyComponent={
<View
style={{
width: "100%",
justifyContent: "center",
alignItems: "center",
height: 60
}}
>
<Paragraph color={colors.icon}>No downloads in progress.</Paragraph>
</View>
}
keyExtractor={(item) => item.id}
renderItem={({ item }) => {
return (
<AttachmentItem
attachment={item}
setAttachments={() => {}}
pressable={false}
hideWhenNotDownloading={true}
/>
);
}}
/>
{result?.size ? (
<Button
style={{
width: 250,
borderRadius: 100,
marginTop: 20
}}
onPress={close}
type="accent"
title="Done"
/>
) : !downloading ? (
<View
style={{
flexDirection: "row",
width: "100%",
marginTop: 20
}}
>
<Button
style={{
flex: 1,
borderRadius: 100,
marginRight: 5
}}
onPress={close}
type="grayBg"
title="No"
/>
<Button
style={{
flex: 1,
borderRadius: 100,
marginLeft: 5
}}
onPress={onDownload}
type="accent"
title="Yes"
/>
</View>
) : (
<Button
style={{
width: 250,
borderRadius: 100,
marginTop: 20
}}
onPress={cancel}
type="error"
title="Cancel"
/>
)}
</View>
);
};
DownloadAttachments.present = (context, attachments, isNote) => {
presentSheet({
context: context,
component: (ref, close, update) => (
<DownloadAttachments
close={close}
attachments={attachments}
isNote={isNote}
update={update}
/>
)
});
};
export default DownloadAttachments;

View File

@@ -18,60 +18,44 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useRef, useState } from "react";
import { ActivityIndicator, ScrollView, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import { View } from "react-native";
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 { useThemeStore } from "../../stores/use-theme-store";
import { SIZE } from "../../utils/size";
import SheetProvider from "../sheet-provider";
import { IconButton } from "../ui/icon-button";
import DialogHeader from "../dialog/dialog-header";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { AttachmentItem } from "./attachment-item";
import DownloadAttachments from "./download-attachments";
import { Button } from "../ui/button";
import {
isAudio,
isDocument,
isImage,
isVideo
} from "@notesnook/core/utils/filename";
import { FlatList } from "react-native-actions-sheet";
export const AttachmentDialog = ({ note }) => {
export const AttachmentDialog = ({ data }) => {
const colors = useThemeStore((state) => state.colors);
const [note, setNote] = useState(data);
const [attachments, setAttachments] = useState(
note
? db.attachments.ofNote(note.id, "all")
data
? db.attachments.ofNote(data.id, "all")
: [...(db.attachments.all || [])]
);
const attachmentSearchValue = useRef();
const searchTimer = useRef();
const [loading, setLoading] = useState(false);
const [currentFilter, setCurrentFilter] = useState("all");
const onChangeText = (text) => {
const attachments = note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
attachmentSearchValue.current = text;
if (
!attachmentSearchValue.current ||
attachmentSearchValue.current === ""
) {
setAttachments([...attachments]);
setAttachments([...db.attachments.all]);
}
clearTimeout(searchTimer.current);
searchTimer.current = setTimeout(() => {
let results = db.lookup.attachments(
attachments,
db.attachments.all,
attachmentSearchValue.current
);
if (results.length === 0) return;
@@ -83,77 +67,6 @@ export const AttachmentDialog = ({ note }) => {
<AttachmentItem setAttachments={setAttachments} attachment={item} />
);
const onCheck = async () => {
setLoading(true);
const checkedAttachments = [];
for (let attachment of attachments) {
let result = await filesystem.checkAttachment(attachment.metadata.hash);
if (result.failed) {
await db.attachments.markAsFailed(
attachment.metadata.hash,
result.failed
);
} else {
await db.attachments.markAsFailed(attachment.id, null);
}
checkedAttachments.push(
db.attachments.attachment(attachment.metadata.hash)
);
setAttachments([...checkedAttachments]);
}
setLoading(false);
};
const attachmentTypes = [
{
title: "All",
filterBy: "all"
},
{
title: "Images",
filterBy: "images"
},
{
title: "Documents",
filterBy: "documents"
},
{
title: "Video",
filterBy: "video"
},
{
title: "Audio",
filterBy: "audio"
}
];
const filterAttachments = (type) => {
const attachments = note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
isDocument;
switch (type) {
case "all":
return attachments;
case "images":
return attachments.filter((attachment) =>
isImage(attachment.metadata.type)
);
case "video":
return attachments.filter((attachment) =>
isVideo(attachment.metadata.type)
);
case "audio":
return attachments.filter((attachment) =>
isAudio(attachment.metadata.type)
);
case "documents":
return attachments.filter((attachment) =>
isDocument(attachment.metadata.type)
);
}
};
return (
<View
style={{
@@ -162,71 +75,43 @@ export const AttachmentDialog = ({ note }) => {
paddingHorizontal: 12
}}
>
<SheetProvider context="attachments-list" />
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center"
}}
>
<Heading>Attachments</Heading>
<View
style={{
flexDirection: "row"
}}
>
{loading ? (
<ActivityIndicator
style={{
height: 40,
width: 40,
marginRight: 10
}}
size={SIZE.lg}
/>
) : (
<IconButton
name="check-all"
customStyle={{
height: 40,
width: 40,
marginRight: 10
}}
color={colors.pri}
size={SIZE.lg}
onPress={onCheck}
/>
)}
<IconButton
name="download"
customStyle={{
height: 40,
width: 40
}}
color={colors.pri}
onPress={() => {
DownloadAttachments.present(
"attachments-list",
attachments,
!!note
<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
);
}}
size={SIZE.lg}
/>
</View>
</View>
<Seperator />
<Input
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current);
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
keyboardDismissMode="none"
@@ -234,45 +119,6 @@ export const AttachmentDialog = ({ note }) => {
maxToRenderPerBatch={10}
initialNumToRender={10}
windowSize={5}
stickyHeaderIndices={[0]}
ListHeaderComponent={
<ScrollView
style={{
width: "100%",
height: 50,
flexDirection: "row",
backgroundColor: colors.bg
}}
contentContainerStyle={{
minWidth: "100%"
}}
horizontal
>
{attachmentTypes.map((item) => (
<Button
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
key={item.title}
title={
item.title +
` (${filterAttachments(item.filterBy)?.length || 0})`
}
style={{
borderRadius: 0,
borderBottomWidth: 1,
flexGrow: 1,
borderBottomColor:
currentFilter !== item.filterBy
? "transparent"
: colors.accent
}}
onPress={() => {
setCurrentFilter(item.filterBy);
setAttachments(filterAttachments(item.filterBy));
}}
/>
))}
</ScrollView>
}
ListEmptyComponent={
<View
style={{
@@ -316,6 +162,6 @@ export const AttachmentDialog = ({ note }) => {
AttachmentDialog.present = (note) => {
presentSheet({
component: () => <AttachmentDialog note={note} />
component: () => <AttachmentDialog data={note} />
});
};

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { Platform, View } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import {
eSubscribeEvent,
@@ -75,7 +75,7 @@ const AuthModal = () => {
return !visible ? null : (
<BaseDialog
overlayOpacity={0}
statusBarTranslucent={true}
statusBarTranslucent={false}
onRequestClose={currentAuthMode !== AuthMode.welcomeSignup && close}
visible={true}
onClose={close}
@@ -84,7 +84,6 @@ const AuthModal = () => {
background={colors.bg}
transparent={false}
animated={false}
avoidKeyboardResize
>
{currentAuthMode !== AuthMode.login ? (
<Signup
@@ -102,7 +101,7 @@ const AuthModal = () => {
<View
style={{
position: "absolute",
top: insets.top,
top: Platform.OS === "ios" ? insets.top : 0,
zIndex: 999,
flexDirection: "row",
alignItems: "center",
@@ -127,11 +126,11 @@ const AuthModal = () => {
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
<Button
title="Skip"
title="Skip for now"
onPress={() => {
hideAuth();
}}
iconSize={16}
iconSize={20}
type="gray"
iconPosition="right"
icon="chevron-right"

View File

@@ -18,8 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useState } from "react";
import { TouchableOpacity, View } from "react-native";
import { View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
import { DDS } from "../../services/device-detection";
import { eSendEvent } from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
@@ -29,12 +30,13 @@ import SheetProvider from "../sheet-provider";
import { Progress } from "../sheets/progress";
import { Button } from "../ui/button";
import Input from "../ui/input";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { SVG } from "./background";
import { hideAuth } from "./common";
import { ForgotPassword } from "./forgot-password";
import { useLogin } from "./use-login";
import { useSettingStore } from "../../stores/use-setting-store";
const LoginSteps = {
emailAuth: 1,
@@ -62,7 +64,6 @@ export const Login = ({ changeMode }) => {
await sleep(500);
Progress.present();
});
const deviceMode = useSettingStore((state) => state.deviceMode);
useEffect(() => {
async () => {
@@ -81,70 +82,62 @@ export const Login = ({ changeMode }) => {
<>
<ForgotPassword />
<SheetProvider context="two_factor_verify" />
<View
<Animated.View
entering={FadeInDown}
exiting={FadeOutUp}
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.bg,
zIndex: 10,
width: "100%",
alignSelf: "center",
height: "100%",
minHeight: "100%"
}}
>
<View
style={{
flexGrow: 0.8,
justifyContent: "flex-end",
paddingHorizontal: 20,
backgroundColor: colors.nav,
borderBottomWidth: 1,
marginBottom: 12,
borderBottomColor: colors.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
borderWidth: deviceMode !== "mobile" ? 1 : null,
borderColor: deviceMode !== "mobile" ? colors.border : null,
borderRadius: deviceMode !== "mobile" ? 20 : null,
marginTop: deviceMode !== "mobile" ? 50 : null,
width: deviceMode === "mobile" ? null : "50%"
height: 250,
overflow: "hidden"
}}
>
<SvgView
src={SVG(colors.night ? colors.icon : "black")}
height={700}
/>
</View>
<View
style={{
width: "100%",
justifyContent: "center",
alignSelf: "center",
paddingHorizontal: 12,
marginBottom: 30,
marginTop: 15
}}
>
<View
style={{
flexDirection: "row"
}}
>
<View
style={{
width: 100,
height: 5,
backgroundColor: colors.accent,
borderRadius: 2,
marginRight: 7
}}
/>
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.nav,
borderRadius: 2
}}
/>
</View>
<Heading
style={{
marginBottom: 25,
marginTop: 10
textAlign: "center"
}}
extraBold
size={SIZE.xxl}
size={30}
color={colors.heading}
>
Login to your {"\n"}account
Welcome back!
</Heading>
<Paragraph
style={{
textDecorationLine: "underline",
textAlign: "center",
marginTop: 5
}}
onPress={() => {
if (loading) return;
changeMode(1);
}}
size={SIZE.md}
>
{"Don't have an account? Sign up"}
</Paragraph>
</View>
<View
style={{
width: DDS.isTab
@@ -154,10 +147,10 @@ export const Login = ({ changeMode }) => {
: focused
? "100%"
: "99.9%",
padding: 12,
backgroundColor: colors.bg,
alignSelf: "center",
paddingHorizontal: 20,
flexGrow: 1
flexGrow: 1,
alignSelf: "center"
}}
>
<Input
@@ -224,22 +217,28 @@ export const Login = ({ changeMode }) => {
<View
style={{
marginTop: 25
marginTop: 25,
alignSelf: "center"
}}
>
<Button
style={{
width: 250,
borderRadius: 100
}}
loading={loading}
onPress={() => {
if (loading) return;
login();
}}
style={{
width: 250,
borderRadius: 100
}}
fontSize={SIZE.md}
type="accent"
title={!loading ? "Continue" : null}
title={
loading
? null
: step === LoginSteps.emailAuth
? "Login"
: "Continue"
}
/>
{step === LoginSteps.passwordAuth && (
@@ -262,34 +261,9 @@ export const Login = ({ changeMode }) => {
type="errorShade"
/>
)}
{!loading ? (
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(1);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: 12,
paddingVertical: 12
}}
>
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
Don't have an account?{" "}
<Paragraph
size={SIZE.xs + 1}
style={{ color: colors.accent }}
>
Sign up
</Paragraph>
</Paragraph>
</TouchableOpacity>
) : null}
</View>
</View>
</View>
</Animated.View>
</>
);
};

View File

@@ -18,7 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useRef, useState } from "react";
import { TouchableOpacity, View } from "react-native";
import { Dimensions, View } from "react-native";
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { ToastEvent } from "../../services/event-manager";
@@ -31,10 +32,11 @@ import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import Input from "../ui/input";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { SVG } from "./background";
import { hideAuth } from "./common";
import { useSettingStore } from "../../stores/use-setting-store";
export const Signup = ({ changeMode, trial }) => {
const colors = useThemeStore((state) => state.colors);
@@ -48,7 +50,6 @@ export const Signup = ({ changeMode, trial }) => {
const [loading, setLoading] = useState(false);
const setUser = useUserStore((state) => state.setUser);
const setLastSynced = useUserStore((state) => state.setLastSynced);
const deviceMode = useSettingStore((state) => state.deviceMode);
const validateInfo = () => {
if (!password.current || !email.current || !confirmPassword.current) {
@@ -95,77 +96,69 @@ export const Signup = ({ changeMode, trial }) => {
return (
<>
<View
<Animated.View
entering={FadeInDown}
exiting={FadeOutUp}
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.bg,
zIndex: 10,
width: "100%",
alignSelf: "center",
height: "100%",
minHeight: "100%"
}}
>
<View
style={{
flexGrow: 1,
justifyContent: "flex-end",
paddingHorizontal: 20,
backgroundColor: colors.nav,
marginBottom: 20,
borderBottomWidth: 1,
borderBottomColor: colors.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
borderWidth: deviceMode !== "mobile" ? 1 : null,
borderColor: deviceMode !== "mobile" ? colors.border : null,
borderRadius: deviceMode !== "mobile" ? 20 : null,
marginTop: deviceMode !== "mobile" ? 50 : null,
width: deviceMode === "mobile" ? null : "50%"
height: 250,
overflow: "hidden"
}}
>
<View
style={{
flexDirection: "row"
}}
>
<View
style={{
width: 100,
height: 5,
backgroundColor: colors.accent,
borderRadius: 2,
marginRight: 7
}}
/>
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.nav,
borderRadius: 2
}}
/>
</View>
<Heading
extraBold
style={{
marginBottom: 25,
marginTop: 10
}}
size={SIZE.xxl}
>
Create your {"\n"}account
</Heading>
<SvgView
src={SVG(colors.night ? colors.icon : "black")}
height={700}
/>
</View>
<View
style={{
width: DDS.isTab ? "50%" : "100%",
paddingHorizontal: 20,
backgroundColor: colors.bg,
width: "100%",
justifyContent: "center",
alignSelf: "center",
flexGrow: 0.5
paddingHorizontal: 12,
marginBottom: 30,
marginTop: Dimensions.get("window").height < 700 ? -75 : 15
}}
>
<Heading
style={{
textAlign: "center"
}}
size={30}
color={colors.heading}
>
Create your account
</Heading>
<Paragraph
style={{
textDecorationLine: "underline",
textAlign: "center"
}}
onPress={() => {
if (loading) return;
changeMode(0);
}}
size={SIZE.md}
>
Already have an account? Log in
</Paragraph>
</View>
<View
style={{
width: DDS.isTab ? "50%" : "100%",
padding: 12,
backgroundColor: colors.bg,
flexGrow: 1,
alignSelf: "center"
}}
>
<Input
@@ -224,13 +217,37 @@ export const Signup = ({ changeMode, trial }) => {
validationType="confirmPassword"
customValidator={() => password.current}
placeholder="Confirm password"
marginBottom={12}
marginBottom={5}
onSubmit={signup}
/>
<View
style={{
marginTop: 25,
alignSelf: "center"
}}
>
<Button
style={{
width: 250,
borderRadius: 100
}}
loading={loading}
onPress={() => {
if (loading) return;
signup();
}}
type="accent"
title={loading ? null : "Agree and continue"}
/>
</View>
<Paragraph
style={{
marginBottom: 25
textAlign: "center",
position: "absolute",
bottom: 0,
alignSelf: "center",
marginBottom: 20
}}
size={SIZE.xs}
color={colors.icon}
@@ -246,7 +263,7 @@ export const Signup = ({ changeMode, trial }) => {
}}
color={colors.accent}
>
Terms of Service{" "}
terms of service{" "}
</Paragraph>
and{" "}
<Paragraph
@@ -259,46 +276,11 @@ export const Signup = ({ changeMode, trial }) => {
}}
color={colors.accent}
>
Privacy Policy.
</Paragraph>{" "}
You also agree to recieve marketing emails from us which you can
opt-out of from app settings.
</Paragraph>
<Button
title={!loading ? "Continue" : null}
type="accent"
loading={loading}
onPress={signup}
fontSize={SIZE.md}
style={{
marginRight: 12,
width: 250,
borderRadius: 100
}}
/>
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(0);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: 12,
paddingVertical: 12
}}
>
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
Already have an account?{" "}
<Paragraph size={SIZE.xs + 1} style={{ color: colors.accent }}>
Login
</Paragraph>
privacy policy.
</Paragraph>
</TouchableOpacity>
</Paragraph>
</View>
</View>
</Animated.View>
</>
);
};

View File

@@ -36,7 +36,6 @@ import SheetProvider from "../sheet-provider";
import RateAppSheet from "../sheets/rate-app";
import RecoveryKeySheet from "../sheets/recovery-key";
import RestoreDataSheet from "../sheets/restore-data";
import PDFPreview from "../dialogs/pdf-preview";
const DialogProvider = () => {
const colors = useThemeStore((state) => state.colors);
@@ -61,7 +60,6 @@ const DialogProvider = () => {
{loading ? null : <Expiring />}
<AnnouncementDialog />
<SessionExpired />
<PDFPreview />
</>
);
};

View File

@@ -46,8 +46,7 @@ const BaseDialog = ({
animated = true,
bounce = true,
closeOnTouch = true,
useSafeArea = true,
avoidKeyboardResize = false
useSafeArea = true
}) => {
const floating = useIsFloatingKeyboard();
@@ -95,7 +94,7 @@ const BaseDialog = ({
}}
>
<KeyboardAvoidingView
enabled={!floating && Platform.OS === "ios" && !avoidKeyboardResize}
enabled={!floating && Platform.OS === "ios"}
behavior="padding"
>
<BouncingView

View File

@@ -1,324 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef, useState } from "react";
import { Dimensions, TextInput, View } from "react-native";
import {
addOrientationListener,
removeOrientationListener
} from "react-native-orientation";
import Pdf from "react-native-pdf";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import downloadAttachment from "../../../common/filesystem/download-attachment";
import { cacheDir } from "../../../common/filesystem/utils";
import { useAttachmentProgress } from "../../../hooks/use-attachment-progress";
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../../services/event-manager";
import { useThemeStore } from "../../../stores/use-theme-store";
import { SIZE } from "../../../utils/size";
import { Dialog } from "../../dialog";
import BaseDialog from "../../dialog/base-dialog";
import { presentDialog } from "../../dialog/functions";
import SheetProvider from "../../sheet-provider";
import { IconButton } from "../../ui/icon-button";
import { ProgressBarComponent } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
import { sleep } from "../../../utils/time";
import { MMKV } from "../../../common/database/mmkv";
const WIN_WIDTH = Dimensions.get("window").width;
const WIN_HEIGHT = Dimensions.get("window").height;
const attachmentSnapshotsKey = "___attachmentsnapshots";
const usePDFSnapshot = (attachment) => {
const snapshots = useRef(MMKV.getMap(attachmentSnapshotsKey) || {});
const snapshot = useRef(snapshots[attachment?.id]);
function saveSnapshot(ss) {
if (!attachment) return;
snapshots.current[attachment.id] = ss;
MMKV.setMap(attachmentSnapshotsKey, snapshots.current);
snapshot.current = snapshots.current[attachment.id];
}
return [snapshot, saveSnapshot];
};
const PDFPreview = () => {
const colors = useThemeStore((state) => state.colors);
const [visible, setVisible] = useState(false);
const [pdfSource, setPDFSource] = useState();
const [loading, setLoading] = useState(false);
const [width, setWidth] = useState(WIN_WIDTH);
const insets = useGlobalSafeAreaInsets();
const [numPages, setNumPages] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const inputRef = useRef();
const pdfRef = useRef();
const [attachment, setAttachment] = useState(null);
const [password, setPassword] = useState("");
const [progress] = useAttachmentProgress(attachment);
const [snapshot, saveSnapshot] = usePDFSnapshot(attachment);
const snapshotValue = useRef(snapshot.current);
useEffect(() => {
eSubscribeEvent("PDFPreview", open);
return () => {
eUnSubscribeEvent("PDFPreview", open);
};
}, []);
const onOrientationChange = (o) => {
if (o.includes("LANDSCAPE")) {
setWidth(WIN_HEIGHT);
} else {
setWidth(WIN_WIDTH);
}
};
useEffect(() => {
addOrientationListener(onOrientationChange);
return () => {
removeOrientationListener(onOrientationChange);
};
}, []);
const open = async (attachment) => {
setVisible(true);
setLoading(true);
setTimeout(async () => {
setAttachment(attachment);
let hash = attachment.metadata.hash;
if (!hash) return;
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
});
const path = `${cacheDir}/${uri}`;
snapshotValue.current = snapshot.current;
setPDFSource("file://" + path);
setLoading(false);
}, 100);
};
const close = () => {
setPDFSource(null);
setVisible(false);
setPassword("");
};
const onError = async (error) => {
if (error?.message === "Password required or incorrect password.") {
await sleep(300);
presentDialog({
context: attachment?.metadata?.hash,
input: true,
inputPlaceholder: "Enter password",
positiveText: "Unlock",
title: "Decrypt",
paragraph: "Please input password to view pdf.",
positivePress: (value) => {
setTimeout(() => {
setPassword(value);
});
},
onClose: () => {
close();
}
});
}
};
return (
visible && (
<BaseDialog animation="fade" visible={true} onRequestClose={close}>
<SheetProvider context={attachment?.metadata?.hash} />
<Dialog context={attachment?.metadata?.hash} />
<View
style={{
width: "100%",
height: "100%",
backgroundColor: "black"
}}
>
{loading ? (
<Animated.View
exiting={FadeOut}
style={{
flex: 1,
justifyContent: "center",
alignItems: "center"
}}
>
<ProgressBarComponent
indeterminate={!progress}
color={colors.accent}
borderColor="transparent"
progress={parseInt(progress?.value || "100") / 100}
/>
<Paragraph
style={{
marginTop: 10
}}
color={colors.light}
>
Loading {`${progress?.percent ? `(${progress?.percent})` : ""}`}
... Please wait
</Paragraph>
</Animated.View>
) : (
<>
<View
style={{
width: "100%",
height: 50,
marginTop: insets.top,
flexDirection: "row",
justifyContent: "space-between",
paddingHorizontal: 12,
paddingLeft: 6
}}
>
<View
style={{
flexDirection: "row"
}}
>
<IconButton
color={colors.light}
name="arrow-left"
onPress={close}
customStyle={{
marginRight: 12
}}
size={SIZE.xxl}
/>
</View>
<View
style={{
flexDirection: "row",
alignItems: "center",
marginRight: 12
}}
>
<TextInput
ref={inputRef}
defaultValue={currentPage + ""}
style={{
color: colors.pri,
padding: 0,
paddingTop: 0,
paddingBottom: 0,
marginTop: 0,
marginBottom: 0,
paddingVertical: 0,
height: 25,
backgroundColor: colors.nav,
width: 40,
textAlign: "center",
marginRight: 4,
borderRadius: 3,
fontFamily: "OpenSans-Regular"
}}
selectTextOnFocus
keyboardType="decimal-pad"
onSubmitEditing={(event) => {
setCurrentPage(event.nativeEvent.text);
pdfRef.current?.setPage(parseInt(event.nativeEvent.text));
}}
blurOnSubmit
/>
<Paragraph color={colors.light}>/{numPages}</Paragraph>
</View>
<View
style={{
flexDirection: "row"
}}
>
<IconButton
color={colors.light}
name="download"
onPress={() => {
downloadAttachment(attachment.metadata.hash, false);
}}
/>
</View>
</View>
{pdfSource ? (
<Animated.View
style={{
flex: 1
}}
entering={FadeIn}
>
<Pdf
source={{
uri: pdfSource
}}
ref={pdfRef}
onLoadComplete={(numberOfPages) => {
setNumPages(numberOfPages);
}}
onPageChanged={(page) => {
setCurrentPage(page);
inputRef.current?.setNativeProps({
text: page + ""
});
saveSnapshot({
currentPage: page,
scale: snapshot?.current?.scale
});
}}
// scale={snapshotValue.current?.scale}
// onScaleChanged={(scale) => {
// saveSnapshot({
// currentPage: snapshot?.current?.currentPage,
// scale: scale
// });
// }}
page={snapshotValue?.current?.currentPage}
password={password}
maxScale={6}
onError={onError}
onPressLink={(uri) => {
console.log(`Link pressed: ${uri}`);
}}
style={{
flex: 1,
width: width,
height: Dimensions.get("window").height
}}
/>
</Animated.View>
) : null}
</>
)}
</View>
</BaseDialog>
)
);
};
export default PDFPreview;

View File

@@ -86,7 +86,7 @@ export const ProFeatures = ({ count = 6 }) => {
await sleep(300);
eSendEvent(eOpenPremiumDialog);
}}
size={SIZE.xs}
size={SIZE.xs + 1}
style={{
textDecorationLine: "underline",
color: colors.icon

File diff suppressed because one or more lines are too long

View File

@@ -42,6 +42,7 @@ import { eOpenAnnouncementDialog } from "../../utils/events";
import { getGithubVersion } from "../../utils/github-version";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { SVG } from "../auth/background";
import Migrate from "../sheets/migrate";
import NewFeature from "../sheets/new-feature/index";
import { Update } from "../sheets/update";
@@ -49,6 +50,7 @@ import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { Walkthrough } from "../walkthroughs";
@@ -68,7 +70,6 @@ const Launcher = React.memo(
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const verifying = useRef(false);
const loadNotes = useCallback(async () => {
if (verifyUser) {
@@ -87,7 +88,7 @@ const Launcher = React.memo(
const init = useCallback(async () => {
if (!db.isInitialized) {
RNBootSplash.hide({ fade: true });
await RNBootSplash.hide({ fade: true });
DatabaseLogger.info("Initializing database");
await db.init();
}
@@ -130,6 +131,7 @@ const Launcher = React.memo(
if (NewFeature.present()) return;
if (await checkAppUpdateAvailable()) return;
if (await checkForRateAppRequest()) return;
if (await checkNeedsBackup()) return;
if (await PremiumService.getRemainingTrialDaysStatus()) return;
if (introCompleted) {
@@ -172,6 +174,23 @@ const Launcher = React.memo(
return false;
};
const checkNeedsBackup = async () => {
return false;
// let { nextBackupRequestTime, reminder } = SettingsService.get();
// if (reminder === 'off' || !reminder) {
// if (nextBackupRequestTime < Date.now()) {
// presentSheet({
// title: 'Backup & restore',
// paragraph: 'Please enable automatic backups to keep your data safe',
// component: <SettingsBackupAndRestore isSheet={true} />
// });
// return true;
// }
// }
// return false;
};
const onUnlockBiometrics = useCallback(async () => {
if (!(await BiometricService.isBiometryAvailable())) return;
if (Platform.OS === "android") {
@@ -187,9 +206,6 @@ const Launcher = React.memo(
setVerifyUser(false);
enabled(false);
password.current = null;
setTimeout(() => {
verifying.current = false;
}, 1);
}
}, [setVerifyUser]);
@@ -198,10 +214,7 @@ const Launcher = React.memo(
}, [init, verifyUser]);
useEffect(() => {
if (verifying.current || useUserStore.getState().shouldBlockVerifyUser)
return;
if (verifyUser && appState === "active") {
verifying.current = true;
onUnlockBiometrics();
}
}, [appState, onUnlockBiometrics, verifyUser]);
@@ -227,10 +240,18 @@ const Launcher = React.memo(
width: "100%",
height: "100%",
position: "absolute",
zIndex: 999,
justifyContent: "center"
zIndex: 999
}}
>
<View
style={{
height: 250,
overflow: "hidden"
}}
>
<SvgView src={SVG(colors.night ? "white" : "black")} height={700} />
</View>
<View
style={{
flex: 1,
@@ -266,13 +287,14 @@ const Launcher = React.memo(
textAlign: "center"
}}
>
Unlock your notes
Unlock to access your notes
</Heading>
<Paragraph
style={{
alignSelf: "center",
textAlign: "center",
fontSize: SIZE.md,
maxWidth: "90%"
}}
>
@@ -283,7 +305,8 @@ const Launcher = React.memo(
style={{
width: "100%",
padding: 12,
backgroundColor: colors.bg
backgroundColor: colors.bg,
flexGrow: 1
}}
>
{user ? (
@@ -323,9 +346,14 @@ const Launcher = React.memo(
<Button
title="Unlock with Biometrics"
width={250}
height={45}
style={{
borderRadius: 100
}}
onPress={onUnlockBiometrics}
icon={"fingerprint"}
type={user ? "grayAccent" : "accent"}
fontSize={SIZE.md}
/>
</View>
</View>

View File

@@ -178,29 +178,16 @@ const NoteItem = ({
</View>
) : null}
{compactMode ? (
<Paragraph
numberOfLines={1}
color={COLORS_NOTE[item.color?.toLowerCase()] || colors.heading}
style={{
flexWrap: "wrap"
}}
size={SIZE.sm}
>
{item.title}
</Paragraph>
) : (
<Heading
numberOfLines={1}
color={COLORS_NOTE[item.color?.toLowerCase()] || colors.heading}
style={{
flexWrap: "wrap"
}}
size={SIZE.md}
>
{item.title}
</Heading>
)}
<Heading
numberOfLines={1}
color={COLORS_NOTE[item.color?.toLowerCase()] || colors.heading}
style={{
flexWrap: "wrap"
}}
size={SIZE.md}
>
{item.title}
</Heading>
{item.headline && !compactMode ? (
<Paragraph
@@ -215,219 +202,161 @@ const NoteItem = ({
</Paragraph>
) : null}
{compactMode ? null : (
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
width: "100%",
marginTop: 5,
height: SIZE.md + 2
}}
>
{!isTrash ? (
<>
{item.conflicted ? (
<Icon
name="alert-circle"
style={{
marginRight: 6
}}
size={SIZE.sm}
color={colors.red}
/>
) : null}
<TimeSince
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
width: "100%",
marginTop: 5,
height: SIZE.md + 2
}}
>
{!isTrash ? (
<>
{item.conflicted ? (
<Icon
name="alert-circle"
style={{
fontSize: SIZE.xs,
color: colors.icon,
marginRight: 6
}}
time={item[dateBy]}
updateFrequency={
Date.now() - item[dateBy] < 60000 ? 2000 : 60000
size={SIZE.sm}
color={colors.red}
/>
) : null}
<TimeSince
style={{
fontSize: SIZE.xs,
color: colors.icon,
marginRight: 6
}}
time={item[dateBy]}
updateFrequency={
Date.now() - item[dateBy] < 60000 ? 2000 : 60000
}
/>
{attachmentCount > 0 ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
marginRight: 6
}}
>
<Icon name="attachment" size={SIZE.md} color={colors.icon} />
<Paragraph color={colors.icon} size={SIZE.xs}>
{attachmentCount}
</Paragraph>
</View>
) : null}
{item.pinned ? (
<Icon
testID="icon-pinned"
name="pin-outline"
size={SIZE.sm}
style={{
marginRight: 6
}}
color={
COLORS_NOTE[item.color?.toLowerCase()] || colors.accent
}
/>
) : null}
{attachmentCount > 0 ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
marginRight: 6
}}
>
<Icon
name="attachment"
size={SIZE.md}
color={colors.icon}
/>
<Paragraph color={colors.icon} size={SIZE.xs}>
{attachmentCount}
</Paragraph>
</View>
) : null}
{item.pinned ? (
<Icon
testID="icon-pinned"
name="pin-outline"
size={SIZE.sm}
style={{
marginRight: 6
}}
color={
COLORS_NOTE[item.color?.toLowerCase()] || colors.accent
}
/>
) : null}
{item.locked ? (
<Icon
name="lock"
testID="note-locked-icon"
size={SIZE.sm}
style={{
marginRight: 6
}}
color={colors.icon}
/>
) : null}
{item.favorite ? (
<Icon
testID="icon-star"
name="star"
size={SIZE.md}
style={{
marginRight: 6
}}
color="orange"
/>
) : null}
{!isTrash && !compactMode && tags
? tags.map((item) =>
item.id ? (
<Button
title={"#" + item.alias}
key={item.id}
height={23}
type="gray"
textStyle={{
textDecorationLine: "underline"
}}
hitSlop={{ top: 8, bottom: 12, left: 0, right: 0 }}
fontSize={SIZE.xs}
style={{
borderRadius: 5,
paddingHorizontal: 6,
marginRight: 4,
zIndex: 10,
maxWidth: tags.length > 1 ? 130 : null
}}
onPress={() => navigateToTag(item)}
/>
) : null
)
: null}
</>
) : (
<>
<Paragraph
{item.locked ? (
<Icon
name="lock"
testID="note-locked-icon"
size={SIZE.sm}
style={{
marginRight: 6
}}
color={colors.icon}
size={SIZE.xs}
style={{
marginRight: 6
}}
>
Deleted on{" "}
{item && item.dateDeleted
? new Date(item.dateDeleted).toISOString().slice(0, 10)
: null}
</Paragraph>
/>
) : null}
<Paragraph
color={colors.accent}
size={SIZE.xs}
{item.favorite ? (
<Icon
testID="icon-star"
name="star"
size={SIZE.md}
style={{
marginRight: 6
}}
>
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
</Paragraph>
</>
)}
</View>
)}
color="orange"
/>
) : null}
{!isTrash && !compactMode && tags
? tags.map((item) =>
item.id ? (
<Button
title={"#" + item.alias}
key={item.id}
height={23}
type="gray"
textStyle={{
textDecorationLine: "underline"
}}
hitSlop={{ top: 8, bottom: 12, left: 0, right: 0 }}
fontSize={SIZE.xs}
style={{
borderRadius: 5,
paddingHorizontal: 6,
marginRight: 4,
zIndex: 10,
maxWidth: tags.length > 1 ? 130 : null
}}
onPress={() => navigateToTag(item)}
/>
) : null
)
: null}
</>
) : (
<>
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
marginRight: 6
}}
>
Deleted on{" "}
{item && item.dateDeleted
? new Date(item.dateDeleted).toISOString().slice(0, 10)
: null}
</Paragraph>
<Paragraph
color={colors.accent}
size={SIZE.xs}
style={{
marginRight: 6
}}
>
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
</Paragraph>
</>
)}
</View>
</View>
<View
style={{
flexDirection: "row",
<IconButton
testID={notesnook.listitem.menu}
color={colors.pri}
name="dots-horizontal"
size={SIZE.xl}
onPress={() => !noOpen && showActionSheet(item, isTrash)}
customStyle={{
justifyContent: "center",
height: 35,
width: 35,
borderRadius: 100,
alignItems: "center"
}}
>
{item.conflicted ? (
<Icon
name="alert-circle"
style={{
marginRight: 6
}}
size={SIZE.sm}
color={colors.red}
/>
) : null}
{item.locked ? (
<Icon
name="lock"
testID="note-locked-icon"
size={SIZE.sm}
style={{
marginRight: 6
}}
color={colors.icon}
/>
) : null}
{item.favorite ? (
<Icon
testID="icon-star"
name="star-outline"
size={SIZE.md}
style={{
marginRight: 6
}}
color="orange"
/>
) : null}
<TimeSince
style={{
fontSize: SIZE.xs,
color: colors.icon,
marginRight: 6
}}
time={item[dateBy]}
updateFrequency={Date.now() - item[dateBy] < 60000 ? 2000 : 60000}
/>
<IconButton
testID={notesnook.listitem.menu}
color={colors.pri}
name="dots-horizontal"
size={SIZE.xl}
onPress={() => !noOpen && showActionSheet(item, isTrash)}
customStyle={{
justifyContent: "center",
height: 35,
width: 35,
borderRadius: 100,
alignItems: "center"
}}
/>
</View>
/>
</>
);
};

View File

@@ -31,7 +31,6 @@ import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { getFormattedDate } from "../../../utils/time";
const showActionSheet = (item) => {
Properties.present(item);
@@ -64,28 +63,15 @@ export const NotebookItem = ({
flexShrink: 1
}}
>
{compactMode ? (
<Paragraph
size={SIZE.sm}
numberOfLines={1}
style={{
flexWrap: "wrap"
}}
>
{item.title}
</Paragraph>
) : (
<Heading
size={SIZE.md}
numberOfLines={1}
style={{
flexWrap: "wrap"
}}
>
{item.title}
</Heading>
)}
<Heading
size={SIZE.md}
numberOfLines={1}
style={{
flexWrap: "wrap"
}}
>
{item.title}
</Heading>
{isTopic || !item.description || compactMode ? null : (
<Paragraph
size={SIZE.sm}
@@ -136,51 +122,40 @@ export const NotebookItem = ({
</View>
)}
{!compactMode ? (
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
marginTop: 5,
height: SIZE.md + 2
}}
>
{isTrash ? (
<>
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
textAlignVertical: "center",
marginRight: 6
}}
>
{"Deleted on " +
new Date(item.dateDeleted).toISOString().slice(0, 10)}
</Paragraph>
<Paragraph
color={colors.accent}
size={SIZE.xs}
style={{
textAlignVertical: "center",
marginRight: 6
}}
>
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
</Paragraph>
</>
) : (
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
marginTop: 5,
height: SIZE.md + 2
}}
>
{isTrash ? (
<>
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
textAlignVertical: "center",
marginRight: 6
}}
>
{getFormattedDate(item[dateBy], "date")}
{"Deleted on " +
new Date(item.dateDeleted).toISOString().slice(0, 10)}
</Paragraph>
)}
<Paragraph
color={colors.accent}
size={SIZE.xs}
style={{
textAlignVertical: "center",
marginRight: 6
}}
>
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
</Paragraph>
</>
) : (
<Paragraph
color={colors.icon}
size={SIZE.xs}
@@ -188,61 +163,50 @@ export const NotebookItem = ({
marginRight: 6
}}
>
{item && totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
: "0 notes"}
{new Date(item[dateBy]).toDateString().substring(4)}
</Paragraph>
)}
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
marginRight: 6
}}
>
{item && totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
: "0 notes"}
</Paragraph>
{item.pinned ? (
<Icon
name="pin-outline"
size={SIZE.sm}
style={{
marginRight: 10,
marginTop: 2
}}
color={colors.accent}
/>
) : null}
</View>
) : null}
{item.pinned ? (
<Icon
name="pin-outline"
size={SIZE.sm}
style={{
marginRight: 10,
marginTop: 2
}}
color={colors.accent}
/>
) : null}
</View>
</View>
<View
style={{
flexDirection: "row",
<IconButton
color={colors.heading}
name="dots-horizontal"
testID={notesnook.ids.notebook.menu}
size={SIZE.xl}
onPress={() => showActionSheet(item)}
customStyle={{
justifyContent: "center",
height: 35,
width: 35,
borderRadius: 100,
alignItems: "center"
}}
>
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
marginRight: 6
}}
>
{item && totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
: "0 notes"}
</Paragraph>
<IconButton
color={colors.heading}
name="dots-horizontal"
testID={notesnook.ids.notebook.menu}
size={SIZE.xl}
onPress={() => showActionSheet(item)}
customStyle={{
justifyContent: "center",
height: 35,
width: 35,
borderRadius: 100,
alignItems: "center"
}}
/>
</View>
/>
</>
);
};

View File

@@ -108,7 +108,7 @@ const ReminderItem = React.memo(
color={colors.errorText}
/>
<Paragraph
size={SIZE.xs}
size={SIZE.xs + 1}
color={colors.icon}
style={{ marginLeft: 5 }}
>
@@ -133,7 +133,7 @@ const ReminderItem = React.memo(
>
<Icon name="reload" size={SIZE.md} color={colors.accent} />
<Paragraph
size={SIZE.xs}
size={SIZE.xs + 1}
color={colors.icon}
style={{ marginLeft: 5 }}
>
@@ -147,7 +147,7 @@ const ReminderItem = React.memo(
<ReminderTime
reminder={item}
checkIsActive={false}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
style={{
justifyContent: "flex-start",
borderWidth: 0,

View File

@@ -42,16 +42,11 @@ const SelectionWrapper = ({
const notesListMode = useSettingStore(
(state) => state.settings.notesListMode
);
const listMode =
item.type === "notebook" || item.itemType === "notebook"
? notebooksListMode
: notesListMode;
const listMode = item.type === "notebook" ? notebooksListMode : notesListMode;
const compactMode =
(item.type === "notebook" ||
item.itemType === "notebook" ||
item.itemType === "note" ||
item.type === "note") &&
(item.type === "notebook" || item.type === "note") &&
listMode === "compact";
if (item.id !== itemId.current) {
itemId.current = item.id;
}
@@ -82,7 +77,7 @@ const SelectionWrapper = ({
width: "100%",
overflow: "hidden",
paddingHorizontal: 12,
paddingVertical: compactMode ? 4 : 12,
paddingVertical: compactMode ? 8 : 12,
borderRadius: isSheet ? 10 : 0,
marginBottom: isSheet ? 12 : undefined
}}

View File

@@ -23,7 +23,6 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useThemeStore } from "../../../stores/use-theme-store";
import { SIZE } from "../../../utils/size";
import { useSettingStore } from "../../../stores/use-setting-store";
export const SelectionIcon = ({ item }) => {
const colors = useThemeStore((state) => state.colors);
@@ -33,17 +32,6 @@ export const SelectionIcon = ({ item }) => {
(state) => state.selectedItemsList
);
const [selected, setSelected] = useState(false);
const notebooksListMode = useSettingStore(
(state) => state.settings.notebooksListMode
);
const notesListMode = useSettingStore(
(state) => state.settings.notesListMode
);
const listMode = item.type === "notebook" ? notebooksListMode : notesListMode;
const compactMode =
(item.type === "notebook" || item.type === "note") &&
listMode === "compact";
useEffect(() => {
if (selectionMode) {
@@ -66,8 +54,8 @@ export const SelectionIcon = ({ item }) => {
return selectionMode ? (
<View
style={{
width: compactMode ? 30 : 40,
height: compactMode ? 30 : 40,
width: 40,
height: 40,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
@@ -79,7 +67,7 @@ export const SelectionIcon = ({ item }) => {
>
{selected ? (
<Icon
size={compactMode ? SIZE.xl - 2 : SIZE.xl}
size={SIZE.xl}
color={selected ? colors.accent : colors.icon}
name={"check"}
/>

View File

@@ -37,7 +37,7 @@ import { useThemeStore } from "../../stores/use-theme-store";
import { dHeight } from "../../utils";
import { eOnLoadNote, eShowMergeDialog } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { getFormattedDate, sleep } from "../../utils/time";
import { sleep, timeConverter } from "../../utils/time";
import BaseDialog from "../dialog/base-dialog";
import DialogButtons from "../dialog/dialog-buttons";
import DialogContainer from "../dialog/dialog-container";
@@ -166,7 +166,7 @@ const MergeConflicts = () => {
{isCurrent ? "(This Device)" : "(Incoming)"}
</Text>
{"\n"}
{getFormattedDate(contentToKeep?.dateEdited)}
{timeConverter(contentToKeep?.dateEdited)}
</Paragraph>
</View>

View File

@@ -26,7 +26,7 @@ import { presentSheet } from "../../services/event-manager";
import { useThemeStore } from "../../stores/use-theme-store";
import { openLinkInBrowser } from "../../utils/functions";
import { SIZE } from "../../utils/size";
import { getFormattedDate, timeSince } from "../../utils/time";
import { timeConverter, timeSince } from "../../utils/time";
import DialogHeader from "../dialog/dialog-header";
import SheetProvider from "../sheet-provider";
import { PressableButton } from "../ui/pressable";
@@ -63,15 +63,15 @@ export default function NoteHistory({ note, fwdRef }) {
}, []);
const getDate = (start, end) => {
let _start_date = getFormattedDate(start, "date");
let _end_date = getFormattedDate(end + 60000, "date");
let _start_time = getFormattedDate(start, "time");
let _end_time = getFormattedDate(end + 60000, "time");
return `${_start_date} ${_start_time} - ${
_end_date === _start_date ? " " : _end_date + " "
}${_end_time}`;
let _start = timeConverter(start);
let _end = timeConverter(end + 60000);
if (_start === _end) return _start;
let final = _end.lastIndexOf(",");
let part = _end.slice(0, final + 1);
if (_start.includes(part)) {
return _start + " —" + _end.replace(part, "");
}
return _start + " — " + _end;
};
const renderItem = useCallback(

View File

@@ -18,13 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { ScrollView } from "react-native";
import { FeatureBlock } from "./feature";
import { ScrollView } from "react-native-actions-sheet";
export const CompactFeatures = ({
vertical,
features = [],
maxHeight = 600,
maxHeight = 500,
scrollRef
}) => {
let data = vertical
@@ -80,11 +80,14 @@ export const CompactFeatures = ({
return (
<ScrollView
horizontal={!vertical}
nestedScrollEnabled
onMomentumScrollEnd={() => {
scrollRef?.current?.handleChildScrollEnd();
}}
showsHorizontalScrollIndicator={false}
style={{
width: "100%",
maxHeight: maxHeight,
paddingHorizontal: 12
maxHeight: maxHeight
}}
>
{data.map((item) => (

View File

@@ -143,7 +143,7 @@ export const Expiring = () => {
await sleep(300);
eSendEvent(eOpenPremiumDialog, promo);
}}
size={SIZE.xs}
size={SIZE.xs + 1}
style={{
textDecorationLine: "underline",
color: colors.icon,

View File

@@ -41,19 +41,18 @@ export const FeatureBlock = ({
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 12,
marginBottom: 10,
backgroundColor: colors.nav,
borderRadius: 10,
paddingVertical: 12
marginBottom: 10
}}
>
<Icon color={colors.accent} name="check" size={SIZE.lg} />
<Paragraph
style={{
flexWrap: "wrap",
marginLeft: 5,
flexShrink: 1
}}
size={SIZE.sm}
size={SIZE.md}
>
{content}
</Paragraph>

View File

@@ -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 React, { useCallback, useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { View } from "react-native";
import Animated, { FadeInUp, FadeOutUp } from "react-native-reanimated";
import useKeyboard from "../../hooks/use-keyboard";
import { editorState } from "../../screens/editor/tiptap/utils";
import { DDS } from "../../services/device-detection";
import {
eSendEvent,
@@ -31,7 +31,6 @@ import { useThemeStore } from "../../stores/use-theme-store";
import { getElevation } from "../../utils";
import {
eCloseActionSheet,
eCloseSheet,
eOpenPremiumDialog,
eShowGetPremium
} from "../../utils/events";
@@ -40,12 +39,12 @@ import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useCallback } from "react";
export const PremiumToast = ({ context = "global", offset = 0 }) => {
const colors = useThemeStore((state) => state.colors);
const [msg, setMsg] = useState(null);
const timer = useRef();
const keyboard = useKeyboard();
const open = useCallback(
(event) => {
@@ -73,6 +72,7 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
useEffect(() => {
eSubscribeEvent(eShowGetPremium, open);
return () => {
clearTimeout(timer.current);
eUnSubscribeEvent(eShowGetPremium, open);
};
}, [open]);
@@ -80,7 +80,9 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
const onPress = async () => {
open(null);
eSendEvent(eCloseActionSheet);
eSendEvent(eCloseSheet);
if (editorState().isFocused) {
//tiny.call(EditorWebView, tiny.blur);
}
await sleep(300);
eSendEvent(eOpenPremiumDialog);
};
@@ -100,13 +102,9 @@ export const PremiumToast = ({ context = "global", offset = 0 }) => {
flexDirection: "row",
alignSelf: "center",
justifyContent: "space-between",
top: offset + keyboard.keyboardHeight,
top: offset,
maxWidth: DDS.isLargeTablet() ? 400 : "98%"
}}
onTouchEnd={() => {
setMsg(null);
clearTimeout(timer.current);
}}
>
<View
style={{

View File

@@ -232,7 +232,8 @@ export const PricingPlans = ({
width={250}
style={{
paddingHorizontal: 12,
marginBottom: 15
marginBottom: 15,
borderRadius: 100
}}
/>
</>
@@ -397,7 +398,8 @@ export const PricingPlans = ({
style={{
paddingHorizontal: 12,
marginTop: 30,
marginBottom: 10
marginBottom: 10,
borderRadius: 100
}}
/>
{Platform.OS !== "ios" &&

View File

@@ -21,7 +21,7 @@ import React from "react";
import { View } from "react-native";
import { useThemeStore } from "../../stores/use-theme-store";
import { SIZE } from "../../utils/size";
import { getFormattedDate } from "../../utils/time";
import { timeConverter } from "../../utils/time";
import Paragraph from "../ui/typography/paragraph";
export const DateMeta = ({ item }) => {
const colors = useThemeStore((state) => state.colors);
@@ -62,11 +62,11 @@ export const DateMeta = ({ item }) => {
paddingVertical: 3
}}
>
<Paragraph size={SIZE.xs} color={colors.icon}>
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
{getNameFromKey(key)}
</Paragraph>
<Paragraph size={SIZE.xs} color={colors.icon}>
{getFormattedDate(item[key], "date-time")}
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
{timeConverter(item[key])}
</Paragraph>
</View>
);

View File

@@ -108,7 +108,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
backgroundColor: "transparent",
paddingHorizontal: 0
}}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
/>
) : null}
</View>
@@ -191,25 +191,13 @@ Properties.present = (item, buttons = [], isSheet) => {
break;
case "notebook":
props[0] = db.notebooks.notebook(item.id).data;
props.push([
"edit-notebook",
"pin",
"add-shortcut",
"trash",
"default-notebook"
]);
props.push(["edit-notebook", "pin", "add-shortcut", "trash"]);
break;
case "topic":
props[0] = db.notebooks
.notebook(item.notebookId)
.topics.topic(item.id)._topic;
props.push([
"move-notes",
"edit-topic",
"add-shortcut",
"trash",
"default-topic"
]);
props.push(["move-notes", "edit-topic", "add-shortcut", "trash"]);
break;
case "tag":
props[0] = db.tags.tag(item.id);

View File

@@ -115,12 +115,12 @@ export const Items = ({ item, buttons, close }) => {
onPress={item.func}
key={item.id}
testID={"icon-" + item.id}
activeOpacity={1}
customStyle={{
alignSelf: "flex-start",
width: topBarItemWidth,
marginRight: isLast ? 0 : 10,
paddingHorizontal: 0,
width: topBarItemWidth
backgroundColor: "transparent",
paddingHorizontal: 0
}}
>
<PressableButton
@@ -183,7 +183,7 @@ export const Items = ({ item, buttons, close }) => {
horizontal
style={{
paddingHorizontal: 12,
paddingVertical: 12
paddingTop: 12
}}
>
{topBarItems.map(renderTopBarItem)}

View File

@@ -23,18 +23,13 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import Notebook from "../../screens/notebook";
import { TopicNotes } from "../../screens/notes/topic-notes";
import {
eSendEvent,
presentSheet,
ToastEvent
} from "../../services/event-manager";
import { presentSheet, ToastEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useNotebookStore } from "../../stores/use-notebook-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import { eClearEditor } from "../../utils/events";
export default function Notebooks({ note, close, full }) {
const colors = useThemeStore((state) => state.colors);
@@ -117,7 +112,6 @@ export default function Notebooks({ note, close, full }) {
size={SIZE.sm}
onPress={() => {
navigateNotebook(item.id);
eSendEvent(eClearEditor);
close();
}}
>
@@ -140,7 +134,6 @@ export default function Notebooks({ note, close, full }) {
key={topic.id}
onPress={() => {
navigateTopic(topic.id, item.id);
eSendEvent(eClearEditor);
close();
}}
onLongPress={async () => {
@@ -162,7 +155,7 @@ export default function Notebooks({ note, close, full }) {
title={topic.title}
type="gray"
height={30}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
icon="bookmark-outline"
style={{
marginRight: 5,
@@ -191,7 +184,7 @@ export default function Notebooks({ note, close, full }) {
{noteNotebooks.length > 1 && !full ? (
<Button
title={`See all linked notebooks`}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
style={{
alignSelf: "flex-end",
marginRight: 12,

View File

@@ -91,7 +91,7 @@ export const Synced = ({ item, close }) => {
console.error(e);
}
}}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
title="Learn more"
height={30}
type="grayAccent"

View File

@@ -47,7 +47,7 @@ export const Tags = ({ item, close }) => {
>
<Button
onPress={async () => {
ManageTagsSheet.present([item]);
ManageTagsSheet.present(item);
}}
buttonType={{
text: colors.accent
@@ -57,7 +57,7 @@ export const Tags = ({ item, close }) => {
icon="plus"
iconPosition="right"
height={30}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
style={{
marginRight: 5,
borderRadius: 100,
@@ -107,7 +107,7 @@ const TagItem = ({ tag, close }) => {
title={"#" + tag}
type="grayBg"
height={20}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
style={style}
textStyle={{
textDecorationLine: "underline"

View File

@@ -39,7 +39,7 @@ export const Topics = ({ item, close }) => {
height={30}
onPress={() => open(topic)}
icon="bookmark-outline"
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
style={{
marginRight: 5,
paddingHorizontal: 8,

View File

@@ -36,7 +36,6 @@ import MoveNoteSheet from "../sheets/add-to";
import ExportNotesSheet from "../sheets/export-notes";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import ManageTagsSheet from "../sheets/manage-tags";
export const SelectionHeader = React.memo(() => {
const colors = useThemeStore((state) => state.colors);
@@ -210,20 +209,6 @@ export const SelectionHeader = React.memo(() => {
screen === "Notebook" ||
screen === "Reminders" ? null : (
<>
<IconButton
onPress={async () => {
await sleep(100);
ManageTagsSheet.present(selectedItemsList);
}}
customStyle={{
marginLeft: 10
}}
color={colors.pri}
tooltipText="Manage tags"
tooltipPosition={4}
name="pound"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
//setSelectionMode(false);
@@ -239,7 +224,6 @@ export const SelectionHeader = React.memo(() => {
name="plus"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
ExportNotesSheet.present(selectedItemsList);

View File

@@ -196,11 +196,12 @@ const SheetProvider = ({ context = "global" }) => {
accentColor={data.iconColor || "accent"}
accentText="light"
type="accent"
height={40}
height={45}
width={250}
style={{
marginBottom: 25
borderRadius: 100
}}
fontSize={SIZE.md}
/>
) : null}

View File

@@ -309,6 +309,7 @@ export class AddNotebookSheet extends React.Component {
borderRadius: 100,
paddingHorizontal: 24
}}
fontSize={SIZE.md}
onPress={this.addNewNotebook}
/>
</View>

View File

@@ -42,7 +42,6 @@ import { SelectionProvider } from "./context";
import { FilteredList } from "./filtered-list";
import { ListItem } from "./list-item";
import { useItemSelectionStore } from "./store";
import { useRelationStore } from "../../../stores/use-relation-store";
const MoveNoteSheet = ({ note, actionSheetRef }) => {
const colors = useThemeStore((state) => state.colors);
@@ -264,7 +263,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
if (itemState[id] === "selected") {
if (item.type === "notebook") {
for (let noteId of noteIds) {
await db.relations.add(item, { id: noteId, type: "note" });
db.relations.add(item, { id: noteId, type: "note" });
}
} else {
await db.notes.addToNotebook(
@@ -279,7 +278,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
} else if (itemState[id] === "deselected") {
if (item.type === "notebook") {
for (let noteId of noteIds) {
await db.relations.unlink(item, { id: noteId, type: "note" });
db.relations.unlink(item, { id: noteId, type: "note" });
}
} else {
await db.notes.removeFromNotebook(
@@ -297,7 +296,6 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
setNotebooks();
eSendEvent(eOnTopicSheetUpdate);
SearchService.updateAndSearch();
useRelationStore.getState().update();
actionSheetRef.current?.hide();
};

View File

@@ -42,7 +42,6 @@ import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { eSendEvent } from "../../../services/event-manager";
import { eCloseSheet } from "../../../utils/events";
import { requestInAppReview } from "../../../services/app-review";
const ExportNotesSheet = ({ notes, update }) => {
const colors = useThemeStore((state) => state.colors);
@@ -73,7 +72,6 @@ const ExportNotesSheet = ({ notes, update }) => {
update({ disableClosing: false });
setComplete(true);
setExporting(false);
requestInAppReview();
};
const actions = [

View File

@@ -22,7 +22,7 @@ import { View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import { ToastEvent, presentSheet } from "../../../services/event-manager";
import { presentSheet, ToastEvent } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useTagStore } from "../../../stores/use-tag-store";
import { useThemeStore } from "../../../stores/use-theme-store";
@@ -31,10 +31,9 @@ import Input from "../../ui/input";
import { PressableButton } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { IconButton } from "../../ui/icon-button";
const ManageTagsSheet = (props) => {
const colors = useThemeStore((state) => state.colors);
const [notes, setNotes] = useState(props.notes || []);
const [note, setNote] = useState(props.note);
const allTags = useTagStore((state) => state.tags);
const [tags, setTags] = useState([]);
const [query, setQuery] = useState(null);
@@ -43,7 +42,7 @@ const ManageTagsSheet = (props) => {
useEffect(() => {
sortTags();
}, [allTags, notes, query, sortTags]);
}, [allTags, note, query, sortTags]);
const sortTags = useCallback(() => {
let _tags = [...allTags];
@@ -52,14 +51,13 @@ const ManageTagsSheet = (props) => {
if (query) {
_tags = _tags.filter((t) => t.title.startsWith(query));
}
const tagsMerged = [...notes.map((note) => note.tags || []).flat()];
if (!tagsMerged || !tagsMerged.length) {
if (!note || !note.tags) {
setTags(_tags);
return;
}
let noteTags = [];
for (let tag of tagsMerged) {
for (let tag of note.tags) {
let index = _tags.findIndex((t) => t.title === tag);
if (index !== -1) {
noteTags.push(_tags[index]);
@@ -69,7 +67,7 @@ const ManageTagsSheet = (props) => {
noteTags = noteTags.sort((a, b) => a.title.localeCompare(b.title));
let combinedTags = [...noteTags, ..._tags];
setTags(combinedTags);
}, [allTags, notes, query]);
}, [allTags, note, query]);
useEffect(() => {
useTagStore.getState().setTags();
@@ -87,23 +85,15 @@ const ManageTagsSheet = (props) => {
}
let tag = _query;
setNotes(
notes.map((note) => ({
...note,
tags: note.tags ? [...note.tags, tag] : [tag]
}))
);
setNote({ ...note, tags: note.tags ? [...note.tags, tag] : [tag] });
setQuery(null);
inputRef.current?.setNativeProps({
text: ""
});
try {
for (let note of notes) {
await db.notes.note(note.id).tag(tag);
}
await db.notes.note(note.id).tag(tag);
useTagStore.getState().setTags();
setNotes(notes.map((note) => db.notes.note(note.id).data));
setNote(db.notes.note(note.id).data);
} catch (e) {
ToastEvent.show({
heading: "Cannot add tag",
@@ -190,91 +180,84 @@ const ManageTagsSheet = (props) => {
) : null}
{tags.map((item) => (
<TagItem
key={item.title}
tag={item}
notes={notes}
setNotes={setNotes}
/>
<TagItem key={item.title} tag={item} note={note} setNote={setNote} />
))}
</ScrollView>
</View>
);
};
ManageTagsSheet.present = (notes) => {
ManageTagsSheet.present = (note) => {
presentSheet({
component: (ref) => {
return <ManageTagsSheet actionSheetRef={ref} notes={notes} />;
return <ManageTagsSheet actionSheetRef={ref} note={note} />;
}
});
};
export default ManageTagsSheet;
const TagItem = ({ tag, notes, setNotes }) => {
const TagItem = ({ tag, note, setNote }) => {
const colors = useThemeStore((state) => state.colors);
const someNotesTagged = notes.some(
(note) => note.tags?.indexOf(tag.title) !== -1
);
const allNotesTagged = notes.every(
(note) => note.tags?.indexOf(tag.title) !== -1
);
const onPress = async () => {
for (let note of notes) {
try {
if (someNotesTagged) {
await db.notes
.note(note.id)
.untag(note.tags[note.tags.indexOf(tag.title)]);
} else {
await db.notes.note(note.id).tag(tag.title);
}
} catch (e) {
console.error(e);
let prevNote = { ...note };
try {
if (prevNote.tags.indexOf(tag.title) !== -1) {
await db.notes
.note(note.id)
.untag(prevNote.tags[prevNote.tags.indexOf(tag.title)]);
} else {
await db.notes.note(note.id).tag(tag.title);
}
useTagStore.getState().setTags();
setNote(db.notes.note(note.id).data);
} catch (e) {
console.error(e);
}
useTagStore.getState().setTags();
setNotes(notes.map((note) => db.notes.note(note.id).data));
setTimeout(() => {
Navigation.queueRoutesForUpdate();
}, 1);
};
return (
<PressableButton
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "flex-start",
height: 40
justifyContent: "space-between",
padding: 12
}}
onPress={onPress}
type="gray"
type={
note && note.tags.findIndex((t) => t === tag.title) !== -1
? "shade"
: "grayBg"
}
>
<IconButton
size={22}
customStyle={{
marginRight: 5,
width: 23,
height: 23
}}
color={someNotesTagged || allNotesTagged ? colors.accent : colors.icon}
testID={
allNotesTagged
? "check-circle-outline"
: someNotesTagged
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
<Heading
size={SIZE.sm}
color={
note && note?.tags.findIndex((t) => t === tag.title) !== -1
? colors.accent
: colors.pri
}
>
{"#" + tag.title}
</Heading>
<Icon
name={
allNotesTagged
? "check-circle-outline"
: someNotesTagged
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
note && note?.tags.findIndex((t) => t === tag.title) !== -1
? "minus"
: "plus"
}
color={
note && note?.tags.findIndex((t) => t === tag.title) !== -1
? colors.accent
: colors.accent
}
size={SIZE.lg}
/>
<Paragraph size={SIZE.sm}>{"#" + tag.alias}</Paragraph>
</PressableButton>
);
};

View File

@@ -158,7 +158,11 @@ export const MoveNotes = ({
{item.title}
</Paragraph>
{item.type == "note" && item.headline ? (
<Paragraph numberOfLines={1} color={colors.icon} size={SIZE.xs}>
<Paragraph
numberOfLines={1}
color={colors.icon}
size={SIZE.xs + 1}
>
{item.headline}
</Paragraph>
) : null}

View File

@@ -35,7 +35,6 @@ import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { requestInAppReview } from "../../../services/app-review";
const PublishNoteSheet = ({ note: item, update }) => {
const colors = useThemeStore((state) => state.colors);
@@ -67,7 +66,6 @@ const PublishNoteSheet = ({ note: item, update }) => {
Navigation.queueRoutesForUpdate();
setPublishLoading(false);
}
requestInAppReview();
} catch (e) {
ToastEvent.show({
heading: "Could not publish note",

View File

@@ -43,7 +43,8 @@ import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import { QRCode } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
import RNFetchBlob from "react-native-blob-util";
let RNFetchBlob;
class RecoveryKeySheet extends React.Component {
constructor(props) {
@@ -114,6 +115,7 @@ class RecoveryKeySheet extends React.Component {
this.svg.current?.toDataURL(async (data) => {
try {
let path;
RNFetchBlob = (await import("rn-fetch-blob")).default;
let fileName = "nn_" + this.user.email + "_recovery_key_qrcode";
fileName = sanitizeFilename(fileName, { replacement: "_" });
fileName = fileName + ".png";
@@ -149,6 +151,7 @@ class RecoveryKeySheet extends React.Component {
fileName = sanitizeFilename(fileName, { replacement: "_" });
fileName = fileName + ".txt";
RNFetchBlob = (await import("rn-fetch-blob")).default;
if (Platform.OS === "android") {
let file = await ScopedStorage.createDocument(
fileName,

View File

@@ -119,7 +119,7 @@ export default function ReminderNotify({
marginTop: 10
}}
>
<Paragraph size={SIZE.xs}>Remind me in:</Paragraph>
<Paragraph size={SIZE.xs + 1}>Remind me in:</Paragraph>
{QuickActions.map((item) => {
return (
<Button
@@ -127,7 +127,7 @@ export default function ReminderNotify({
key={item.title}
title={item.title}
height={30}
fontSize={SIZE.xs}
fontSize={SIZE.xs + 1}
style={{ marginLeft: 10, borderRadius: 100 }}
onPress={() => onSnooze(item.time)}
/>
@@ -150,7 +150,7 @@ export default function ReminderNotify({
<Paragraph
style={{
color: colors.icon,
fontSize: SIZE.xs,
fontSize: SIZE.xs + 1,
marginBottom: 10
}}
>

View File

@@ -243,6 +243,7 @@ export default function ReminderSheet({
borderRadius: 100,
paddingHorizontal: 24
}}
fontSize={SIZE.md}
onPress={saveReminder}
/>
</View>
@@ -549,7 +550,7 @@ export default function ReminderSheet({
}}
>
<>
<Paragraph size={SIZE.xs} color={colors.icon}>
<Paragraph size={SIZE.xs + 1} color={colors.icon}>
{recurringMode === RecurringModes.Daily
? "Repeats daily " + `at ${dayjs(date).format("hh:mm A")}.`
: selectedDays.length === 7 &&

View File

@@ -21,22 +21,21 @@ import { EVENTS } from "@notesnook/core/common";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { ActivityIndicator, Platform, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import RNFetchBlob from "react-native-blob-util";
import DocumentPicker from "react-native-document-picker";
import * as ScopedStorage from "react-native-scoped-storage";
import { db } from "../../../common/database";
import storage from "../../../common/database/storage";
import {
ToastEvent,
eSubscribeEvent,
eUnSubscribeEvent
eUnSubscribeEvent,
ToastEvent
} from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { initialize } from "../../../stores";
import { useThemeStore } from "../../../stores/use-theme-store";
import { eCloseRestoreDialog, eOpenRestoreDialog } from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import { getFormattedDate } from "../../../utils/time";
import { timeConverter } from "../../../utils/time";
import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header";
import { presentDialog } from "../../dialog/functions";
@@ -45,7 +44,7 @@ import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import Paragraph from "../../ui/typography/paragraph";
let RNFetchBlob;
const RestoreDataSheet = () => {
const [visible, setVisible] = useState(false);
const [restoring, setRestoring] = useState(false);
@@ -214,6 +213,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
return;
}
} else {
RNFetchBlob = (await import("rn-fetch-blob")).default;
let path = await storage.checkAndCreateDir("/backups/");
files = await RNFetchBlob.fs.lstat(path);
}
@@ -251,7 +251,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
}}
>
<Paragraph size={SIZE.sm} style={{ width: "100%", maxWidth: "100%" }}>
{getFormattedDate(item?.lastModified * 1)}
{timeConverter(item?.lastModified * 1)}
</Paragraph>
<Paragraph size={SIZE.xs}>
{(item.filename || item.name).replace(".nnbackup", "")}

View File

@@ -65,6 +65,7 @@ import { deleteItems } from "../../../utils/functions";
import { presentDialog } from "../../dialog/functions";
import { Properties } from "../../properties";
import Sort from "../sort";
import Heading from "../../ui/typography/heading";
type ConfigItem = { id: string; type: string };
class TopicSheetConfig {

View File

@@ -302,7 +302,6 @@ export const FluidTabs = forwardRef<TabsRef, TabProps>(function FluidTabs(
translateX.value = value;
})
.onEnd((event) => {
if (locked.value || forcedLock.value) return;
if (currentTab.value === 2 && Platform.OS === "android") return;
const velocityX =
event.velocityX < 0 ? event.velocityX * -1 : event.velocityX;

View File

@@ -40,8 +40,6 @@ import { getElevation } from "../../../utils";
import { SIZE } from "../../../utils/size";
import { IconButton } from "../icon-button";
import Paragraph from "../typography/paragraph";
import phone from "phone";
interface InputProps extends TextInputProps {
fwdRef?: RefObject<TextInput>;
validationType?:
@@ -112,14 +110,14 @@ const Input = ({
const [secureEntry, setSecureEntry] = useState(true);
const [showError, setShowError] = useState(false);
const [errorList, setErrorList] = useState({
SHORT_PASS: false
SHORT_PASS: true
});
type ErrorKey = keyof typeof errorList;
const color = error
? colors.red
: focus
? customColor || colors.accent
: colors.border;
: colors.nav;
const validate = async (value: string) => {
if (!validationType) return;
@@ -151,6 +149,7 @@ const Input = ({
isError = customValidator && value === customValidator();
break;
case "phonenumber": {
const { default: phone } = await import("phone");
const result = phone(value, {
strictDetection: true,
validateMobilePrefix: true
@@ -187,12 +186,6 @@ const Input = ({
onChangeText && onChangeText(value);
setShowError(false);
validate(value);
if (value === "") {
setError(false);
setErrorList({
SHORT_PASS: false
});
}
};
const onBlur = () => {

View File

@@ -65,7 +65,7 @@ export const Notice = ({
flexShrink: 1
}}
selectable={selectable}
size={isSmall ? SIZE.xs : SIZE.sm}
size={isSmall ? SIZE.xs + 1 : SIZE.sm}
>
{text}
</Paragraph>

View File

@@ -16,15 +16,17 @@ 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 { isReminderActive } from "@notesnook/core/collections/reminders";
import React from "react";
import {
formatReminderTime,
isReminderActive
} from "@notesnook/core/collections/reminders";
import { ViewStyle } from "react-native";
import { Reminder } from "../../../services/notifications";
import { Button, ButtonProps } from "../button";
import { useThemeStore } from "../../../stores/use-theme-store";
import { SIZE } from "../../../utils/size";
import { getFormattedReminderTime } from "../../../utils/time";
import { Button, ButtonProps } from "../button";
export const ReminderTime = ({
checkIsActive = true,
@@ -40,7 +42,7 @@ export const ReminderTime = ({
} & ButtonProps) => {
const colors = useThemeStore((state) => state.colors);
const reminder = props.reminder;
const time = !reminder ? undefined : getFormattedReminderTime(reminder);
const time = !reminder ? undefined : formatReminderTime(reminder);
const isTodayOrTomorrow =
(time?.includes("Today") || time?.includes("Tomorrow")) &&
!time?.includes("Last");

View File

@@ -105,9 +105,6 @@ const SheetWrapper = ({
defaultOverlayOpacity={overlayOpacity}
overlayColor={pitchBlack ? "#585858" : "#2b2b2b"}
keyboardShouldPersistTaps="always"
openAnimationConfig={{
friction: 9
}}
ExtraOverlayComponent={
<>
{overlay}

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useMemo } from "react";
import { Platform, Text, TextProps, ViewStyle } from "react-native";
import { Platform, Text, TextProps } from "react-native";
import Animated, {
ComplexAnimationBuilder,
Layout
@@ -30,26 +30,15 @@ interface HeadingProps extends TextProps {
size?: number;
layout?: ComplexAnimationBuilder;
animated?: boolean;
extraBold?: boolean;
}
const AnimatedText = Animated.createAnimatedComponent(Text);
const extraBoldStyle = {
fontFamily: Platform.OS === "android" ? "OpenSans-Bold" : undefined,
fontWeight: Platform.OS === "ios" ? "800" : undefined
};
const boldStyle = {
fontFamily: Platform.OS === "android" ? "OpenSans-SemiBold" : undefined,
fontWeight: Platform.OS === "ios" ? "600" : undefined
};
const Heading = ({
color,
size = SIZE.xl,
style,
animated,
extraBold,
...restProps
}: HeadingProps) => {
const colors = useThemeStore((state) => state.colors);
@@ -64,9 +53,11 @@ const Heading = ({
style={[
{
fontSize: size || SIZE.xl,
color: color || colors.heading
color: color || colors.heading,
fontFamily:
Platform.OS === "android" ? "OpenSans-SemiBold" : undefined,
fontWeight: Platform.OS === "ios" ? "600" : undefined
},
extraBold ? (extraBoldStyle as ViewStyle) : (boldStyle as ViewStyle),
style
]}
></Component>

View File

@@ -19,4 +19,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { FeatureType } from "./components/sheets/new-feature";
export const features: FeatureType[] = [];
export const features: FeatureType[] = [
{
title: "Default font size & font family",
body: "Now you can set default font size and font family in editor that will be used across all your new and old notes."
}
];

View File

@@ -71,9 +71,6 @@ export const useActions = ({ close = () => null, item }) => {
const user = useUserStore((state) => state.user);
const [notifPinned, setNotifPinned] = useState(null);
const alias = item.alias || item.title;
const [defaultNotebook, setDefaultNotebook] = useState(
db.settings.getDefaultNotebook()
);
const isPublished =
item.type === "note" && db.monographs.isPublished(item.id);
@@ -384,12 +381,11 @@ export const useActions = ({ close = () => null, item }) => {
positivePress: async (value) => {
if (!value || value === "" || value.trimStart().length == 0) return;
await db.tags.rename(item.id, db.tags.sanitize(value));
setTimeout(() => {
setImmediate(() => {
useTagStore.getState().setTags();
useMenuStore.getState().setMenuPins();
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
}, 1);
});
},
input: true,
defaultValue: alias,
@@ -524,7 +520,7 @@ export const useActions = ({ close = () => null, item }) => {
}
async function showAttachments() {
AttachmentDialog.present(item);
AttachmentDialog.present();
}
async function exportNote() {
@@ -617,7 +613,7 @@ export const useActions = ({ close = () => null, item }) => {
},
{
id: "pin",
title: item.pinned ? "Unpin" : "Pin",
title: "Pin",
icon: item.pinned ? "pin-off-outline" : "pin-outline",
func: pinItem,
close: false,
@@ -627,7 +623,7 @@ export const useActions = ({ close = () => null, item }) => {
},
{
id: "favorite",
title: item.favorite ? "Unfavorite" : "Favorite",
title: "Favorite",
icon: item.favorite ? "star-off" : "star-outline",
func: addToFavorites,
close: false,
@@ -705,7 +701,7 @@ export const useActions = ({ close = () => null, item }) => {
},
{
id: "read-only",
title: "Readonly",
title: "Read only",
icon: "pencil-lock",
func: toggleReadyOnlyMode,
on: item.readonly
@@ -777,53 +773,6 @@ export const useActions = ({ close = () => null, item }) => {
func: openHistory
},
{
id: "default-notebook",
title:
defaultNotebook?.id === item.id
? "Remove as default"
: "Set as default",
hidden: item.type !== "notebook",
icon: "notebook",
func: async () => {
if (defaultNotebook?.id === item.id) {
await db.settings.setDefaultNotebook();
setDefaultNotebook();
} else {
const notebook = {
id: item.id
};
await db.settings.setDefaultNotebook(notebook);
setDefaultNotebook(notebook);
}
close();
},
on: defaultNotebook?.topic ? false : defaultNotebook?.id === item.id
},
{
id: "default-topic",
title:
defaultNotebook?.id === item.id
? "Remove as default"
: "Set as default",
hidden: item.type !== "topic",
icon: "bookmark",
func: async () => {
if (defaultNotebook?.topic === item.id) {
await db.settings.setDefaultNotebook();
setDefaultNotebook();
} else {
const notebook = {
id: item.notebookId,
topic: item.id
};
await db.settings.setDefaultNotebook(notebook);
setDefaultNotebook(notebook);
}
close();
},
on: defaultNotebook?.topic === item.id
},
{
id: "disable-reminder",
title: !item.disabled ? "Turn off reminder" : "Turn on reminder",

View File

@@ -17,37 +17,22 @@ 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 { EV, EVENTS, SYNC_CHECK_IDS } from "@notesnook/core/common";
import notifee from "@notifee/react-native";
import NetInfo from "@react-native-community/netinfo";
import { useCallback, useEffect, useRef } from "react";
import { EV, EVENTS, SYNC_CHECK_IDS } from "@notesnook/core/common";
import { useEffect, useRef } from "react";
import {
AppState,
Appearance,
Keyboard,
AppState,
Linking,
NativeEventEmitter,
NativeModules,
Platform
Platform,
Keyboard
} from "react-native";
import * as RNIap from "react-native-iap";
import { enabled } from "react-native-privacy-snapshot";
import { DatabaseLogger, db } from "../common/database";
import { MMKV } from "../common/database/mmkv";
import { Walkthrough } from "../components/walkthroughs";
import {
clearAppState,
editorController,
editorState
} from "../screens/editor/tiptap/utils";
import { useDragState } from "../screens/settings/editor/state";
import BackupService from "../services/backup";
import {
ToastEvent,
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
} from "../services/event-manager";
import { editorController, editorState } from "../screens/editor/tiptap/utils";
import {
clearMessage,
setEmailVerifyMessage,
@@ -56,23 +41,36 @@ import {
} from "../services/message";
import PremiumService from "../services/premium";
import SettingsService from "../services/settings";
import Sync from "../services/sync";
import { initAfterSync } from "../stores";
import { useAttachmentStore } from "../stores/use-attachment-store";
import { useEditorStore } from "../stores/use-editor-store";
import { useMessageStore } from "../stores/use-message-store";
import { useNoteStore } from "../stores/use-notes-store";
import { useSettingStore } from "../stores/use-setting-store";
import { SyncStatus, useUserStore } from "../stores/use-user-store";
import { updateStatusBarColor } from "../utils/color-scheme";
import { DatabaseLogger, db } from "../common/database";
import { MMKV } from "../common/database/mmkv";
import {
eClearEditor,
eCloseSheet,
eOnLoadNote,
refreshNotesPage
} from "../utils/events";
import Sync from "../services/sync";
import { initAfterSync } from "../stores";
import { SyncStatus, useUserStore } from "../stores/use-user-store";
import { useMessageStore } from "../stores/use-message-store";
import { useSettingStore } from "../stores/use-setting-store";
import { useAttachmentStore } from "../stores/use-attachment-store";
import { useNoteStore } from "../stores/use-notes-store";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
} from "../services/event-manager";
import { useEditorStore } from "../stores/use-editor-store";
import { useDragState } from "../screens/settings/editor/state";
import { useCallback } from "react";
import { clearAppState } from "../screens/editor/tiptap/utils";
import { tabBarRef } from "../utils/global-refs";
import BackupService from "../services/backup";
import { sleep } from "../utils/time";
import notifee from "@notifee/react-native";
const SodiumEventEmitter = new NativeEventEmitter(NativeModules.Sodium);
export const useAppEvents = () => {
@@ -491,8 +489,7 @@ export const useAppEvents = () => {
if (
SettingsService.get().appLockMode === "background" &&
!useSettingStore.getState().requestBiometrics &&
!useUserStore.getState().verifyUser &&
!useUserStore.getState().shouldBlockVerifyUser
!useUserStore.getState().verifyUser
) {
useUserStore.getState().setVerifyUser(true);
if (Platform.OS === "ios") {
@@ -564,6 +561,7 @@ export const useAppEvents = () => {
await db.initCollections();
await db.notes.init();
}
useNoteStore.getState().setNotes();
eSendEvent(refreshNotesPage);
MMKV.removeItem("notesAddedFromIntent");
initAfterSync();

View File

@@ -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 { useEffect, useState } from "react";
import { useState } from "react";
import { useAttachmentStore } from "../stores/use-attachment-store";
type AttachmentProgress = {
@@ -41,26 +41,22 @@ export const useAttachmentProgress = (
: undefined
);
useEffect(() => {
const attachmentProgress = progress?.[attachment?.metadata?.hash];
if (attachmentProgress) {
const type = attachmentProgress.type;
const loaded =
attachmentProgress.type === "download"
? attachmentProgress.recieved
: attachmentProgress.sent;
const value = loaded / attachmentProgress.total;
setCurrentProgress({
value: value * 100,
percent: (value * 100).toFixed(0) + "%",
type: type
});
} else {
setTimeout(() => {
setCurrentProgress(undefined);
}, 300);
}
}, [attachment, progress]);
const attachmentProgress = progress?.[attachment.metadata.hash];
if (attachmentProgress) {
const type = attachmentProgress.type;
const loaded =
attachmentProgress.type === "download"
? attachmentProgress.recieved
: attachmentProgress.sent;
const value = loaded / attachmentProgress.total;
setCurrentProgress({
value: value * 100,
percent: (value * 100).toFixed(0) + "%",
type: type
});
} else {
setCurrentProgress(undefined);
}
return [currentProgress, setCurrentProgress];
};

View File

@@ -50,7 +50,6 @@ import { useSettingStore } from "../stores/use-setting-store";
import { useThemeStore } from "../stores/use-theme-store";
import { history } from "../utils";
import { rootNavigatorRef } from "../utils/global-refs";
import Auth from "../components/auth";
const NativeStack = createNativeStackNavigator();
const IntroStack = createNativeStackNavigator();
@@ -65,7 +64,6 @@ const IntroStack = createNativeStackNavigator();
const IntroStackNavigator = () => {
const colors = useThemeStore((state) => state.colors);
const height = useSettingStore((state) => state.dimensions.height);
return (
<IntroStack.Navigator
screenOptions={{
@@ -73,14 +71,12 @@ const IntroStackNavigator = () => {
lazy: false,
animation: "none",
contentStyle: {
backgroundColor: colors.bg,
minHeight: height
backgroundColor: colors.bg
}
}}
initialRouteName={"Intro"}
>
<NativeStack.Screen name="Intro" component={Intro} />
<NativeStack.Screen name="Auth" component={Auth} />
<NativeStack.Screen name="AppLock" component={AppLock} />
</IntroStack.Navigator>
);
@@ -124,7 +120,7 @@ const _Tabs = () => {
animation: "none",
contentStyle: {
backgroundColor: colors.bg,
minHeight: !introCompleted ? undefined : screenHeight
height: !introCompleted ? undefined : screenHeight
}
}}
>

View File

@@ -271,13 +271,13 @@ const _TabsHolder = () => {
setTimeout(() => {
switch (current) {
case "tablet":
tabBarRef.current?.goToIndex(0, false);
introCompleted && tabBarRef.current?.goToIndex(0, false);
break;
case "smallTablet":
if (!fullscreen) {
tabBarRef.current?.closeDrawer(false);
introCompleted && tabBarRef.current?.closeDrawer(false);
} else {
tabBarRef.current?.openDrawer(false);
introCompleted && tabBarRef.current?.openDrawer(false);
}
break;
case "mobile":
@@ -416,66 +416,64 @@ const _TabsHolder = () => {
backgroundColor="transparent"
/>
{!introCompleted ? (
<NavigationStack />
) : (
<>
{deviceMode && widths[deviceMode] ? (
<FluidTabs
ref={tabBarRef}
dimensions={dimensions}
widths={widths[deviceMode]}
enabled={deviceMode !== "tablet" && !fullscreen}
onScroll={onScroll}
onChangeTab={onChangeTab}
onDrawerStateChange={() => true}
>
<View
key="1"
style={{
height: "100%",
width: fullscreen ? 0 : widths[deviceMode]?.a
{deviceMode && widths[deviceMode] ? (
<FluidTabs
ref={tabBarRef}
dimensions={dimensions}
widths={!introCompleted ? widths["mobile"] : widths[deviceMode]}
enabled={deviceMode !== "tablet" && !fullscreen}
onScroll={onScroll}
onChangeTab={onChangeTab}
onDrawerStateChange={() => true}
>
<View
key="1"
style={{
height: "100%",
width: fullscreen
? 0
: widths[!introCompleted ? "mobile" : deviceMode]?.a
}}
>
<SideMenu />
</View>
<View
key="2"
style={{
height: "100%",
width: fullscreen
? 0
: widths[!introCompleted ? "mobile" : deviceMode]?.b
}}
>
{deviceMode === "mobile" ? (
<Animated.View
onTouchEnd={() => {
tabBarRef.current?.closeDrawer();
animatedOpacity.value = withTiming(0);
animatedTranslateY.value = withTiming(-9999);
}}
>
<SideMenu />
</View>
style={[
{
position: "absolute",
width: "100%",
height: "100%",
zIndex: 999,
backgroundColor: "rgba(0,0,0,0.2)"
},
animatedStyle
]}
ref={overlayRef}
/>
) : null}
<View
key="2"
style={{
height: "100%",
width: fullscreen ? 0 : widths[deviceMode]?.b
}}
>
{deviceMode === "mobile" ? (
<Animated.View
onTouchEnd={() => {
tabBarRef.current?.closeDrawer();
animatedOpacity.value = withTiming(0);
animatedTranslateY.value = withTiming(-9999);
}}
style={[
{
position: "absolute",
width: "100%",
height: "100%",
zIndex: 999,
backgroundColor: "rgba(0,0,0,0.2)"
},
animatedStyle
]}
ref={overlayRef}
/>
) : null}
<NavigationStack />
</View>
<NavigationStack />
</View>
<EditorWrapper key="3" width={widths} dimensions={dimensions} />
</FluidTabs>
) : null}
</>
)}
<EditorWrapper key="3" width={widths} dimensions={dimensions} />
</FluidTabs>
) : null}
</View>
);
};

View File

@@ -27,9 +27,7 @@
"validator": "^13.5.2",
"zustand": "^3.6.0",
"fflate": "^0.7.3",
"timeago.js": "4.0.2",
"react-native-blob-util": "0.17.3",
"react-native-swiper-flatlist": "3.2.2"
"timeago.js": "4.0.2"
},
"sideEffects": false
}

View File

@@ -46,7 +46,6 @@ import { useEditor } from "./tiptap/use-editor";
import { useEditorEvents } from "./tiptap/use-editor-events";
import { editorController } from "./tiptap/utils";
import { useLayoutEffect } from "react";
import useKeyboard from "../../hooks/use-keyboard";
const style: ViewStyle = {
height: "100%",
@@ -129,6 +128,7 @@ const Editor = React.memo(
renderKey.current =
renderKey.current === `editor-0` ? `editor-1` : `editor-0`;
editor.setLoading(true);
setTimeout(() => editor.setLoading(false), 10);
}, [editor]);
useEffect(() => {
@@ -167,7 +167,6 @@ const Editor = React.memo(
injectedJavaScript={`globalThis.sessionId="${editor.sessionId}";`}
javaScriptEnabled={true}
focusable={true}
onContentProcessDidTerminate={onError}
setSupportMultipleWindows={false}
overScrollMode="never"
scrollEnabled={false}
@@ -233,8 +232,6 @@ const AppSection = ({
const ReadonlyButton = ({ editor }: { editor: useEditorType }) => {
const readonly = useEditorStore((state) => state.readonly);
const keyboard = useKeyboard();
const onPress = async () => {
if (editor.note.current) {
await db.notes?.note(editor.note.current.id).readonly();
@@ -244,7 +241,7 @@ const ReadonlyButton = ({ editor }: { editor: useEditorType }) => {
}
};
return readonly && IconButton && !keyboard.keyboardShown ? (
return readonly && IconButton ? (
<IconButton
name="pencil-lock"
type="grayBg"
@@ -252,7 +249,7 @@ const ReadonlyButton = ({ editor }: { editor: useEditorType }) => {
color="accent"
customStyle={{
position: "absolute",
bottom: 60,
bottom: 20,
width: 60,
height: 60,
right: 12,

View File

@@ -101,9 +101,8 @@ const EditorOverlay = ({ editorId = "", editor }) => {
setTimeout(() => {
if (!loadingState.current.startTime) {
translateValue.value = 6000;
opacity.value = 0;
}
}, 3000);
}, 1000);
eSubscribeEvent("loadingNote" + editorId, load);
return () => {
clearTimers();

View File

@@ -217,7 +217,7 @@ typeof globalThis.statusBar !== "undefined" && statusBar.current.set({date:"",sa
);
};
updateWebclip = async ({ src, hash }: Partial<ImageAttributes>) => {
updateWebclip = async ({ src, hash }: ImageAttributes) => {
await this.doAsync(
`editor && editor.commands.updateWebClip(${JSON.stringify({
hash
@@ -225,7 +225,7 @@ typeof globalThis.statusBar !== "undefined" && statusBar.current.set({date:"",sa
);
};
updateImage = async ({ src, hash }: Partial<ImageAttributes>) => {
updateImage = async ({ src, hash }: ImageAttributes) => {
await this.doAsync(
`editor && editor.commands.updateImage(${JSON.stringify({
hash

View File

@@ -22,7 +22,7 @@ import React from "react";
import { Platform, View } from "react-native";
import DocumentPicker from "react-native-document-picker";
import { launchCamera, launchImageLibrary } from "react-native-image-picker";
import RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
import { db } from "../../../common/database";
import { compressToBase64 } from "../../../common/filesystem/compress";
import { AttachmentItem } from "../../../components/attachments/attachment-item";
@@ -34,8 +34,8 @@ import {
import PremiumService from "../../../services/premium";
import { eCloseSheet } from "../../../utils/events";
import { editorController, editorState } from "./utils";
import { isImage } from "@notesnook/core/utils/filename";
import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../../../utils/constants";
const FILE_SIZE_LIMIT = 500 * 1024 * 1024;
const IMAGE_SIZE_LIMIT = 50 * 1024 * 1024;
const showEncryptionSheet = (file) => {
presentSheet({
@@ -74,7 +74,7 @@ const file = async (fileOptions) => {
mode: "import",
allowMultiSelection: false
};
if (Platform.OS === "ios") {
if (Platform.OS == "ios") {
options.copyTo = "cachesDirectory";
}
await db.attachments.generateKey();
@@ -87,9 +87,14 @@ const file = async (fileOptions) => {
}
file = file[0];
let uri = Platform.OS === "ios" ? file.fileCopyUri : file.uri;
if (file.type.startsWith("image")) {
ToastEvent.show({
title: "Type not supported",
message: "Please add images from gallery or camera picker.",
type: "error"
});
return;
}
if (file.size > FILE_SIZE_LIMIT) {
ToastEvent.show({
title: "File too large",
@@ -109,6 +114,7 @@ const file = async (fileOptions) => {
return;
}
let uri = Platform.OS === "ios" ? file.fileCopyUri : file.uri;
console.log("file uri: ", uri);
uri = Platform.OS === "ios" ? santizeUri(uri) : uri;
showEncryptionSheet(file);
@@ -119,24 +125,12 @@ const file = async (fileOptions) => {
if (!(await attachFile(uri, hash, file.type, file.name, fileOptions)))
return;
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
if (isImage(file.type)) {
editorController.current?.commands.insertImage({
hash: hash,
filename: file.name,
mime: file.type,
size: file.size,
dataurl: await db.attachments.read(hash, "base64"),
title: file.name
});
} else {
editorController.current?.commands.insertAttachment({
hash: hash,
filename: file.name,
mime: file.type,
size: file.size
});
}
editorController.current?.commands.insertAttachment({
hash: hash,
filename: file.name,
type: file.type,
size: file.size
});
setTimeout(() => {
eSendEvent(eCloseSheet);
}, 1000);
@@ -261,7 +255,7 @@ const handleImageResponse = async (response, options) => {
editorController.current?.commands.insertImage({
hash: hash,
mime: image.type,
type: image.type,
title: fileName,
dataurl: b64,
size: image.fileSize,
@@ -269,7 +263,7 @@ const handleImageResponse = async (response, options) => {
});
};
export async function attachFile(uri, hash, type, filename, options) {
async function attachFile(uri, hash, type, filename, options) {
try {
let exists = db.attachments.exists(hash);
let encryptionInfo;
@@ -288,7 +282,7 @@ export async function attachFile(uri, hash, type, filename, options) {
let key = await db.attachments.generateKey();
encryptionInfo = await Sodium.encryptFile(key, {
uri: uri,
type: options.type || "url",
type: "url",
hash: hash
});
encryptionInfo.type = type;

View File

@@ -50,8 +50,6 @@ export type Settings = {
corsProxy: string;
fontSize: string;
fontFamily: string;
dateFormat: string;
timeFormat: string;
};
export type EditorProps = {

View File

@@ -130,11 +130,6 @@ export const useEditorEvents = (
const deviceMode = useSettingStore((state) => state.deviceMode);
const fullscreen = useSettingStore((state) => state.fullscreen);
const corsProxy = useSettingStore((state) => state.settings.corsProxy);
const [dateFormat, timeFormat] = useSettingStore((state) => [
state.dateFormat,
state.timeFormat
]);
const handleBack = useRef<NativeEventSubscription>();
const readonly = useEditorStore((state) => state.readonly);
const isPremium = useUserStore((state) => state.premium);
@@ -178,9 +173,7 @@ export const useEditorEvents = (
doubleSpacedLines: doubleSpacedLines,
corsProxy: corsProxy,
fontSize: defaultFontSize,
fontFamily: defaultFontFamily,
dateFormat: db.settings?.getDateFormat(),
timeFormat: db.settings?.getTimeFormat()
fontFamily: defaultFontFamily
});
}, [
fullscreen,
@@ -197,9 +190,7 @@ export const useEditorEvents = (
noToolbar,
corsProxy,
defaultFontSize,
defaultFontFamily,
dateFormat,
timeFormat
defaultFontFamily
]);
const onBackPress = useCallback(async () => {
@@ -358,7 +349,7 @@ export const useEditorEvents = (
});
return;
}
ManageTagsSheet.present([editor.note.current]);
ManageTagsSheet.present(editor.note.current);
break;
case EventTypes.tag:
if (editorMessage.value) {
@@ -407,18 +398,9 @@ export const useEditorEvents = (
openLinkInBrowser(editorMessage.value as string);
break;
case EventTypes.previewAttachment: {
const hash = (editorMessage.value as Attachment)?.hash;
const attachment = db.attachments?.attachment(hash);
if (attachment.metadata.type.startsWith("image/")) {
eSendEvent("ImagePreview", editorMessage.value);
} else {
eSendEvent("PDFPreview", attachment);
}
case EventTypes.previewAttachment:
eSendEvent("ImagePreview", editorMessage.value);
break;
}
default:
break;
}

View File

@@ -38,9 +38,8 @@ import { useTagStore } from "../../../stores/use-tag-store";
import { ThemeStore, useThemeStore } from "../../../stores/use-theme-store";
import { eClearEditor, eOnLoadNote } from "../../../utils/events";
import { tabBarRef } from "../../../utils/global-refs";
import { getFormattedDate } from "../../../utils/time";
import { timeConverter } from "../../../utils/time";
import { NoteType } from "../../../utils/types";
import { onNoteCreated } from "../../notes/common";
import Commands from "./commands";
import { Content, EditorState, Note, SavePayload } from "./types";
import {
@@ -81,8 +80,8 @@ export const useEditor = (
const lockedSessionId = useRef<string>();
const postMessage = useCallback(
async <T>(type: string, data: T, waitFor = 300) =>
await post(editorRef, sessionIdRef.current, type, data, waitFor),
async <T>(type: string, data: T) =>
await post(editorRef, sessionIdRef.current, type, data),
[sessionIdRef]
);
@@ -121,13 +120,9 @@ export const useEditor = (
[editorId]
);
useEffect(() => {
if (loading) {
setLoading(false);
} else {
state.current.ready = false;
}
}, [loading]);
if (loading) {
setLoading(false);
}
const withTimer = useCallback(
(id: string, fn: () => void, duration: number) => {
@@ -199,7 +194,10 @@ export const useEditor = (
sessionId: isContentInvalid(data) ? null : currentSessionHistoryId
};
noteData.title = title;
if (title) {
noteData.title = title;
}
if (data) {
noteData.content = {
data: data,
@@ -211,19 +209,12 @@ export const useEditor = (
id = await db.notes?.add(noteData);
if (!note && id) {
currentNote.current = db.notes?.note(id).data as NoteType;
const defaultNotebook = db.settings?.getDefaultNotebook();
if (!state.current.onNoteCreated && defaultNotebook) {
onNoteCreated(id, {
type: defaultNotebook.topic ? "topic" : "notebook",
id: defaultNotebook.id,
notebook: defaultNotebook.topic
});
} else {
state.current?.onNoteCreated && state.current.onNoteCreated(id);
}
state.current?.onNoteCreated && state.current.onNoteCreated(id);
if (!noteData.title) {
postMessage(EditorEvents.title, currentNote.current.title);
postMessage(
EditorEvents.titleplaceholder,
currentNote.current.title
);
}
}
@@ -248,7 +239,7 @@ export const useEditor = (
}
if (id && sessionIdRef.current === currentSessionId) {
note = db.notes?.note(id)?.data as Note;
await commands.setStatus(getFormattedDate(note.dateEdited), "Saved");
await commands.setStatus(timeConverter(note.dateEdited), "Saved");
lastContentChangeTime.current = note.dateEdited;
@@ -345,6 +336,7 @@ export const useEditor = (
) => {
state.current.currentlyEditing = true;
const editorState = useEditorStore.getState();
if (item && item.type === "new") {
currentNote.current && (await reset());
const nextSessionId = makeSessionId(item as NoteType);
@@ -352,7 +344,7 @@ export const useEditor = (
sessionIdRef.current = nextSessionId;
sessionHistoryId.current = Date.now();
await commands.setSessionId(nextSessionId);
if (state.current?.ready) await commands.focus();
await commands.focus();
lastContentChangeTime.current = 0;
useEditorStore.getState().setReadonly(false);
} else {
@@ -366,7 +358,7 @@ export const useEditor = (
!currentContent.current?.data ||
currentContent.current?.data.length < 50000
) {
if (state.current.ready) overlay(false);
overlay(false);
} else {
overlay(true);
}
@@ -378,13 +370,9 @@ export const useEditor = (
commands.setSessionId(nextSessionId);
sessionIdRef.current = nextSessionId;
currentNote.current = item as NoteType;
await commands.setStatus(getFormattedDate(item.dateEdited), "Saved");
await commands.setStatus(timeConverter(item.dateEdited), "Saved");
await postMessage(EditorEvents.title, item.title);
await postMessage(
EditorEvents.html,
currentContent.current?.data,
10000
);
await postMessage(EditorEvents.html, currentContent.current?.data);
useEditorStore.getState().setReadonly(item.readonly);
await commands.setTags(currentNote.current);
commands.setSettings();
@@ -465,7 +453,7 @@ export const useEditor = (
if (note.tags !== currentNote.current.tags) {
await commands.setTags(note);
}
await commands.setStatus(getFormattedDate(note.dateEdited), "Saved");
await commands.setStatus(timeConverter(note.dateEdited), "Saved");
}
lock.current = false;
@@ -590,10 +578,6 @@ export const useEditor = (
lastContentChangeTime.current = Date.now();
};
useEffect(() => {
state.current.saveCount = 0;
}, [sessionId, loading]);
const onReady = useCallback(async () => {
if (!(await isEditorLoaded(editorRef, sessionIdRef.current))) {
eSendEvent("webview_reset");
@@ -602,24 +586,30 @@ export const useEditor = (
}
}, [isDefaultEditor, restoreEditorState]);
const onLoad = useCallback(async () => {
if (currentNote.current) overlay(true);
clearTimeout(timers.current["editor:loaded"]);
timers.current["editor:loaded"] = setTimeout(async () => {
postMessage(EditorEvents.theme, theme || useThemeStore.getState().colors);
commands.setInsets(
isDefaultEditor ? insets : { top: 0, left: 0, right: 0, bottom: 0 }
);
useEffect(() => {
state.current.saveCount = 0;
(async () => {
await commands.setSessionId(sessionIdRef.current);
await onReady();
await commands.setSettings();
if (currentNote.current) {
loadNote({ ...currentNote.current, forced: true });
} else {
await commands.setPlaceholder(placeholderTip.current);
if (sessionIdRef.current) {
if (!state.current?.ready) return;
await onReady();
}
state.current.ready = true;
}, 300);
})();
}, [sessionId, loading, commands, onReady]);
const onLoad = useCallback(async () => {
state.current.ready = true;
onReady();
postMessage(EditorEvents.theme, theme || useThemeStore.getState().colors);
commands.setInsets(
isDefaultEditor ? insets : { top: 0, left: 0, right: 0, bottom: 0 }
);
if (currentNote.current) {
loadNote({ ...currentNote.current, forced: true });
} else {
await commands.setPlaceholder(placeholderTip.current);
}
commands.setSettings();
}, [
onReady,
postMessage,
@@ -627,8 +617,7 @@ export const useEditor = (
commands,
isDefaultEditor,
insets,
loadNote,
overlay
loadNote
]);
return {

View File

@@ -73,8 +73,7 @@ export async function post<T>(
ref: RefObject<WebView>,
sessionId: string,
type: string,
value: T | null = null,
waitFor = 300
value: T | null = null
) {
if (!sessionId) {
console.warn("post called without sessionId of type:", type);
@@ -86,7 +85,7 @@ export async function post<T>(
sessionId: sessionId
};
setImmediate(() => ref.current?.postMessage(JSON.stringify(message)));
const response = await getResponse(type, waitFor);
const response = await getResponse(type);
return response;
}
@@ -98,8 +97,7 @@ type WebviewResponseData = {
};
export const getResponse = async (
type: string,
waitFor = 300
type: string
): Promise<WebviewResponseData | false> => {
return new Promise((resolve) => {
const callback = (data: WebviewResponseData) => {
@@ -109,7 +107,7 @@ export const getResponse = async (
eSubscribeEvent(type, callback);
setTimeout(() => {
resolve(false);
}, waitFor);
}, 5000);
});
};

View File

@@ -35,7 +35,7 @@ import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { editorRef } from "../../utils/global-refs";
import { ProgressBar } from "./progress";
import { editorController, textInput } from "./tiptap/utils";
import { editorController, editorState, textInput } from "./tiptap/utils";
export const EditorWrapper = ({ width }) => {
const colors = useThemeStore((state) => state.colors);
const deviceMode = useSettingStore((state) => state.deviceMode);
@@ -48,6 +48,7 @@ export const EditorWrapper = ({ width }) => {
const keyboard = useKeyboard();
const onAppStateChanged = async (state) => {
if (editorState().movedAway) return;
if (state === "active") {
editorController.current.onReady();
editorController.current.overlay(false);

View File

@@ -118,7 +118,6 @@ const Notebook = ({ route, navigation }: NavigationProps<"Notebook">) => {
get: () => {
const notebook = db.notebooks?.notebook(params?.current?.item?.id)
?.data as NotebookType;
if (!notebook) return [];
return db.relations?.from(notebook, "note");
}
});

View File

@@ -86,7 +86,7 @@ export const setOnFirstSave = (
}, 0);
};
export async function onNoteCreated(id: string, params: FirstSaveData) {
async function onNoteCreated(id: string, params: FirstSaveData) {
if (!params) return;
switch (params.type) {
case "notebook": {

View File

@@ -28,7 +28,7 @@ import React, {
import { ActivityIndicator, Linking, Platform, View } from "react-native";
import { FlatList } from "react-native-gesture-handler";
import * as ScopedStorage from "react-native-scoped-storage";
import RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
import { db } from "../../common/database";
import Storage from "../../common/database/storage";
import DialogHeader from "../../components/dialog/dialog-header";

View File

@@ -17,36 +17,33 @@ 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 React, { useState } from "react";
import { Dimensions, LayoutAnimation, Platform, View } from "react-native";
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { AuthMode } from "../../components/auth";
import { SVG_Z } from "../../components/intro";
import { WelcomeNotice } from "../../components/intro/welcome";
import { Button } from "../../components/ui/button";
import { PressableButton } from "../../components/ui/pressable";
import Seperator from "../../components/ui/seperator";
import { SvgView } from "../../components/ui/svg";
import { BouncingView } from "../../components/ui/transitions/bouncing-view";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import BiometicService from "../../services/biometrics";
import { DDS } from "../../services/device-detection";
import {
ToastEvent,
eSendEvent,
presentSheet
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { presentSheet, ToastEvent } from "../../services/event-manager";
import SettingsService from "../../services/settings";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeStore } from "../../stores/use-theme-store";
import { useUserStore } from "../../stores/use-user-store";
import { getElevation } from "../../utils";
import { eOpenLoginDialog } from "../../utils/events";
import { SIZE } from "../../utils/size";
const AppLock = ({ route }) => {
const colors = useThemeStore((state) => state.colors);
const appLockMode = useSettingStore((state) => state.settings.appLockMode);
const [step, setStep] = useState(0);
const welcome = route?.params?.welcome;
const deviceMode = useSettingStore((state) => state.deviceMode);
const modes = [
{
@@ -79,12 +76,13 @@ const AppLock = ({ route }) => {
exiting={!welcome ? undefined : FadeOutUp}
entering={!welcome ? undefined : FadeInDown}
style={{
justifyContent: !welcome ? undefined : "center",
height: !welcome ? undefined : "100%",
width: !welcome ? undefined : "100%"
}}
>
<>
{!welcome ? (
{step === 0 ? (
<>
<View
style={{
flexDirection: "row",
@@ -127,163 +125,121 @@ const AppLock = ({ route }) => {
</Paragraph>
</View>
</View>
) : (
<Seperator />
<View
style={{
flexGrow: 1,
justifyContent: "flex-end",
paddingHorizontal: 20,
backgroundColor: colors.nav,
borderBottomWidth: 1,
borderBottomColor: colors.border,
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
borderWidth: deviceMode !== "mobile" ? 1 : null,
borderColor: deviceMode !== "mobile" ? colors.border : null,
borderRadius: deviceMode !== "mobile" ? 20 : null,
marginTop: deviceMode !== "mobile" ? 50 : null,
width: deviceMode === "mobile" ? null : "50%"
paddingHorizontal: 12,
width: DDS.isTab && welcome ? "50%" : "100%",
alignSelf: "center"
}}
>
<View
style={{
flexDirection: "row"
}}
>
<View
style={{
width: 100,
height: 5,
backgroundColor: colors.accent,
borderRadius: 2,
marginRight: 7
{modes.map((item) => (
<PressableButton
key={item.title}
type={appLockMode === item.value ? "grayBg" : "transparent"}
onPress={async () => {
if (
!(await BiometicService.isBiometryAvailable()) &&
!useUserStore.getState().user
) {
ToastEvent.show({
heading: "Biometrics not enrolled",
type: "error",
message:
"To use app lock, you must enable biometrics such as Fingerprint lock or Face ID on your phone or create an account."
});
return;
}
SettingsService.set({ appLockMode: item.value });
}}
/>
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.nav,
borderRadius: 2
customStyle={{
justifyContent: "flex-start",
alignItems: "flex-start",
paddingHorizontal: 12,
paddingVertical: 12,
marginTop: 0,
marginBottom: 12,
borderWidth: 1,
borderColor:
appLockMode === item.value ? item.activeColor : colors.nav
}}
style={{
marginBottom: 10
}}
/>
</View>
<Heading
style={{
marginTop: 10
}}
extraBold
size={SIZE.xxl}
>
Protect your notes
</Heading>
<Paragraph
style={{
marginBottom: 25
}}
>
Choose how you want to secure your notes locally.
</Paragraph>
</View>
)}
<Seperator />
<View
style={{
paddingHorizontal: 12,
width: DDS.isTab && welcome ? "50%" : "100%",
alignSelf: "center",
flexGrow: 1
}}
>
{modes.map((item) => (
<PressableButton
key={item.title}
type={appLockMode === item.value ? "grayBg" : "transparent"}
onPress={async () => {
if (
!(await BiometicService.isBiometryAvailable()) &&
!useUserStore.getState().user
) {
ToastEvent.show({
heading: "Biometrics not enrolled",
type: "error",
message:
"To use app lock, you must enable biometrics such as Fingerprint lock or Face ID on your phone or create an account."
});
return;
}
SettingsService.set({ appLockMode: item.value });
}}
customStyle={{
justifyContent: "flex-start",
alignItems: "flex-start",
paddingHorizontal: 12,
paddingVertical: 12,
marginTop: 0,
marginBottom: 12,
borderWidth: 1,
borderColor:
appLockMode === item.value ? item.activeColor : colors.nav
}}
style={{
marginBottom: 10
}}
>
<Heading
color={
appLockMode === item.value ? item.activeColor : colors.pri
}
style={{ maxWidth: "95%" }}
size={SIZE.md}
>
{item.title}
</Heading>
<Paragraph
color={
appLockMode === item.value ? item.activeColor : colors.icon
}
style={{ maxWidth: "95%" }}
size={SIZE.sm}
>
{item.desc}
</Paragraph>
</PressableButton>
))}
<Heading
color={
appLockMode === item.value ? item.activeColor : colors.pri
}
style={{ maxWidth: "95%" }}
size={SIZE.md}
>
{item.title}
</Heading>
<Paragraph
color={
appLockMode === item.value
? item.activeColor
: colors.icon
}
style={{ maxWidth: "95%" }}
size={SIZE.sm}
>
{item.desc}
</Paragraph>
</PressableButton>
))}
{welcome && (
<Button
fontSize={SIZE.md}
width={250}
onPress={async () => {
eSendEvent(eOpenLoginDialog, AuthMode.welcomeSignup);
setTimeout(() => {
SettingsService.set({
introCompleted: true
});
Navigation.replace(
{
name: "Notes"
},
{
canGoBack: false
{welcome && (
<Button
fontSize={SIZE.md}
height={45}
width={250}
onPress={async () => {
LayoutAnimation.configureNext({
...LayoutAnimation.Presets.linear,
delete: {
duration: 50,
property: "opacity",
type: "linear"
}
);
}, 1000);
}}
style={{
paddingHorizontal: 24,
alignSelf: "center",
...getElevation(5),
marginTop: 30,
borderRadius: 100
}}
type="accent"
title="Next"
/>
)}
</View>
</>
});
setStep(1);
}}
style={{
paddingHorizontal: 24,
alignSelf: "center",
borderRadius: 100,
...getElevation(5),
marginTop: 30
}}
type="accent"
title="Next"
/>
)}
</View>
</>
) : (
<WelcomeNotice />
)}
{welcome && !colors.night ? (
<BouncingView
style={{
position: "absolute",
bottom: DDS.isTab ? -300 : -130,
zIndex: -1
}}
animated={false}
duration={3000}
>
<SvgView
width={Dimensions.get("window").width}
height={Dimensions.get("window").width}
src={SVG_Z}
/>
</BouncingView>
) : null}
</Animated.View>
</>
);

View File

@@ -27,8 +27,6 @@ import SoundPicker from "./sound-picker";
import { Subscription } from "./subscription";
import { TrashIntervalSelector } from "./trash-interval-selector";
import { FontSelector } from "./font-selector";
import { TitleFormat } from "./title-format";
import { DateFormatSelector, TimeFormatSelector } from "./date-format";
export const components: { [name: string]: ReactElement } = {
colorpicker: <AccentColorPicker />,
homeselector: <HomagePageSelector />,
@@ -39,8 +37,5 @@ export const components: { [name: string]: ReactElement } = {
"sound-picker": <SoundPicker />,
licenses: <Licenses />,
"trash-interval-selector": <TrashIntervalSelector />,
"font-selector": <FontSelector />,
"title-format": <TitleFormat />,
"date-format-selector": <DateFormatSelector />,
"time-format-selector": <TimeFormatSelector />
"font-selector": <FontSelector />
};

View File

@@ -1,198 +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 dayjs from "dayjs";
import React, { useRef, useState } from "react";
import { View } from "react-native";
import Menu, { MenuItem } from "react-native-reanimated-material-menu";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { PressableButton } from "../../components/ui/pressable";
import Paragraph from "../../components/ui/typography/paragraph";
import { useThemeStore } from "../../stores/use-theme-store";
import { SIZE } from "../../utils/size";
import { DATE_FORMATS, TIME_FORMATS } from "@notesnook/core/common";
import { useSettingStore } from "../../stores/use-setting-store";
export const DateFormatSelector = () => {
const colors = useThemeStore((state) => state.colors);
const menuRef = useRef();
const [width, setWidth] = useState(0);
const [dateFormat, setDateFormat] = useState(db.settings.getDateFormat());
const onChange = (item) => {
menuRef.current?.hide();
db.settings.setDateFormat(item);
setDateFormat(item);
useSettingStore.setState({
dateFormat: item
});
};
return (
<View
onLayout={(event) => {
setWidth(event.nativeEvent.layout.width);
}}
style={{
width: "100%"
}}
>
<Menu
ref={menuRef}
animationDuration={200}
style={{
borderRadius: 5,
backgroundColor: colors.bg,
width: width,
marginTop: 60
}}
onRequestClose={() => {
menuRef.current?.hide();
}}
anchor={
<PressableButton
onPress={async () => {
menuRef.current?.show();
}}
type="grayBg"
customStyle={{
flexDirection: "row",
alignItems: "center",
marginTop: 10,
width: "100%",
justifyContent: "space-between",
padding: 12
}}
>
<Paragraph>
{dateFormat} ({dayjs().format(dateFormat)})
</Paragraph>
<Icon color={colors.icon} name="menu-down" size={SIZE.md} />
</PressableButton>
}
>
{DATE_FORMATS.map((item) => (
<MenuItem
key={item.id}
onPress={async () => {
onChange(item);
}}
style={{
backgroundColor: dateFormat === item ? colors.nav : "transparent",
width: "100%",
maxWidth: width
}}
textStyle={{
fontSize: SIZE.md,
color: dateFormat === item ? colors.accent : colors.pri
}}
>
{item} ({dayjs().format(item)})
</MenuItem>
))}
</Menu>
</View>
);
};
export const TimeFormatSelector = () => {
const colors = useThemeStore((state) => state.colors);
const menuRef = useRef();
const [width, setWidth] = useState(0);
const [timeFormat, setTimeFormat] = useState(db.settings.getTimeFormat());
const onChange = (item) => {
menuRef.current?.hide();
db.settings.setTimeFormat(item);
setTimeFormat(item);
useSettingStore.setState({
timeFormat: item
});
};
const TimeFormats = {
"12-hour": "hh:mm A",
"24-hour": "HH:mm"
};
return (
<View
onLayout={(event) => {
setWidth(event.nativeEvent.layout.width);
}}
style={{
width: "100%"
}}
>
<Menu
ref={menuRef}
animationDuration={200}
style={{
borderRadius: 5,
backgroundColor: colors.bg,
width: width,
marginTop: 60
}}
onRequestClose={() => {
menuRef.current?.hide();
}}
anchor={
<PressableButton
onPress={async () => {
menuRef.current?.show();
}}
type="grayBg"
customStyle={{
flexDirection: "row",
alignItems: "center",
marginTop: 10,
width: "100%",
justifyContent: "space-between",
padding: 12
}}
>
<Paragraph>
{timeFormat} ({dayjs().format(TimeFormats[timeFormat])})
</Paragraph>
<Icon color={colors.icon} name="menu-down" size={SIZE.md} />
</PressableButton>
}
>
{TIME_FORMATS.map((item) => (
<MenuItem
key={item.id}
onPress={async () => {
onChange(item);
}}
style={{
backgroundColor: timeFormat === item ? colors.nav : "transparent",
width: "100%",
maxWidth: width
}}
textStyle={{
fontSize: SIZE.md,
color: timeFormat === item ? colors.accent : colors.pri
}}
>
{item} ({dayjs().format(TimeFormats[item])})
</MenuItem>
))}
</Menu>
</View>
);
};

View File

@@ -18,12 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Clipboard from "@react-native-clipboard/clipboard";
import { LogMessage } from "@notesnook/logger";
import { LogMessage } from "@streetwriters/logger";
import { format, LogLevel, logManager } from "@notesnook/core/logger";
import React, { useEffect, useState } from "react";
import { FlatList, Platform, TouchableOpacity, View } from "react-native";
import * as ScopedStorage from "react-native-scoped-storage";
import RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
import Storage from "../../common/database/storage";
import { presentDialog } from "../../components/dialog/functions";
import { IconButton } from "../../components/ui/icon-button";

View File

@@ -60,8 +60,8 @@ export const Settings = () => {
<SettingsStack.Navigator
initialRouteName="SettingsHome"
screenListeners={{
focus: (e) => {
if (e.target?.startsWith("SettingsHome-")) {
beforeRemove: (e) => {
if (e.target?.startsWith("SettingsGroup")) {
useNavigationStore.getState().update({ name: "Settings" }, false);
}
}

View File

@@ -268,10 +268,10 @@ export const LICENSES = [
link: "https://github.com/RocketChat/rn-extensions-share"
},
{
name: "react-native-blob-util",
name: "rn-fetch-blob",
licenseType: "MIT",
author: "RonRadtke",
link: "https://github.com/RonRadtke/react-native-blob-util"
author: "Joltup",
link: "https://github.com/joltup/rn-fetch-blob"
},
{
name: "react-native-gzip",

View File

@@ -22,8 +22,8 @@ import {
StackActions,
useNavigation
} from "@react-navigation/native";
import React, { useRef, useState } from "react";
import { View, TextInput, ActivityIndicator } from "react-native";
import React, { useRef } from "react";
import { View, TextInput } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import ToggleSwitch from "toggle-switch-react-native";
import Input from "../../components/ui/input";
@@ -46,14 +46,10 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
const current = item.useHook && item.useHook(item);
const isHidden = item.hidden && item.hidden(item.property || current);
const inputRef = useRef<TextInput>(null);
const [loading, setLoading] = useState(false);
const onChangeSettings = async () => {
if (loading) return;
const onChangeSettings = () => {
if (item.modifer) {
setLoading(true);
await item.modifer(item.property || current);
setLoading(false);
item.modifer(item.property || current);
return;
}
if (!item.property) return;
@@ -293,12 +289,12 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
</View>
</View>
{item.type === "switch" && !loading && (
{item.type === "switch" && item.property && (
<ToggleSwitch
isOn={
item.getter
? item.getter(item.property || current)
: settings[item?.property as never]
: settings[item.property]
}
onColor={colors.accent}
offColor={colors.icon}
@@ -307,10 +303,6 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
onToggle={onChangeSettings}
/>
)}
{loading ? (
<ActivityIndicator size={SIZE.xxl} color={colors.accent} />
) : null}
</PressableButton>
);
};

View File

@@ -179,15 +179,17 @@ export const settingsGroups: SettingSection[] = [
sections: [
{
id: "enable-2fa",
name: "Change primary two-factor authentication",
name: "Enable two-factor authentication",
modifer: () => {
verifyUser("global", async () => {
MFASheet.present();
});
},
useHook: () => useUserStore((state) => state.user),
description:
"Change your current two-factor authentication method"
hidden: (user) => {
return !!(user as User)?.mfa?.isEnabled;
},
description: "Increased security for your account"
},
{
id: "2fa-fallback",
@@ -239,6 +241,23 @@ export const settingsGroups: SettingSection[] = [
},
description:
"View and save recovery codes for to recover your account"
},
{
id: "disabled-2fa",
name: "Disable two-factor authentication",
modifer: () => {
verifyUser("global", async () => {
await db.mfa?.disable();
const user = await db.user?.fetchUser();
useUserStore.getState().setUser(user);
});
},
useHook: () => useUserStore((state) => state.user),
hidden: (user) => {
return !(user as User)?.mfa?.isEnabled;
},
description: "Decreased security for your account"
}
]
},
@@ -505,20 +524,6 @@ export const settingsGroups: SettingSection[] = [
description: "Default screen to open on app startup",
component: "homeselector"
},
{
id: "date-format",
name: "Date format",
description: "Set the format for date used across the app",
type: "component",
component: "date-format-selector"
},
{
id: "time-format",
name: "Time format",
description: "Set the format for time used across the app",
type: "component",
component: "time-format-selector"
},
{
id: "clear-trash-interval",
type: "component",
@@ -526,15 +531,6 @@ export const settingsGroups: SettingSection[] = [
description:
"Select the duration after which trash items will be cleared",
component: "trash-interval-selector"
},
{
id: "default-notebook",
name: "Clear default notebook/topic",
description: "Clear the default notebook/topic for new notes",
modifer: () => {
db.settings?.setDefaultNotebook(undefined);
},
hidden: () => !db.settings?.getDefaultNotebook()
}
]
},
@@ -592,13 +588,6 @@ export const settingsGroups: SettingSection[] = [
icon: "format-font",
property: "defaultFontFamily",
component: "font-selector"
},
{
id: "title-format",
name: "Title format",
component: "title-format",
description: "Customize the formatting for new note title",
type: "component"
}
]
}
@@ -617,26 +606,6 @@ export const settingsGroups: SettingSection[] = [
"Contribute towards a better Notesnook. All tracking information is anonymous.",
property: "telemetry"
},
{
id: "marketing-emails",
type: "switch",
name: "Marketing emails",
description:
"We will send you occasional promotional offers & product updates on your email (sent once every month).",
modifer: async () => {
try {
await db.user?.changeMarketingConsent(
!useUserStore.getState().user?.marketingConsent
);
useUserStore.getState().setUser(await db.user?.fetchUser());
} catch (e) {
ToastEvent.error(e as Error);
}
},
getter: (current: any) => current?.marketingConsent,
useHook: () => useUserStore((state) => state.user),
hidden: (current) => !current
},
{
id: "cors-bypass",
type: "input",

View File

@@ -1,63 +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 { useRef, useState } from "react";
import { db } from "../../common/database";
import Input from "../../components/ui/input";
import React from "react";
import { TextInput } from "react-native";
import Paragraph from "../../components/ui/typography/paragraph";
import { useThemeStore } from "../../stores/use-theme-store";
import { SIZE } from "../../utils/size";
export const TitleFormat = () => {
const [titleFormat] = useState(db.settings?.getTitleFormat());
const inputRef = useRef<TextInput>();
const colors = useThemeStore((state) => state.colors);
return (
<>
<Input
onSubmit={(e) => {
db.settings?.setTitleFormat(e.nativeEvent.text);
}}
onChangeText={(text) => {
db.settings?.setTitleFormat(text);
}}
containerStyle={{ marginTop: 6 }}
onLayout={() => {
inputRef?.current?.setNativeProps({
text: titleFormat
});
}}
defaultValue={titleFormat}
/>
<Paragraph style={{ marginTop: 2 }} color={colors.icon} size={SIZE.xs}>
Use the following key to format the title:{"\n"}
{"\n"}
$date$: Current date.{"\n"}
$time$: Current time.{"\n"}
$timestamp$: Full date and time without any spaces or other symbols.
(e.g 202305261253).{"\n"}
$count$: Number of notes + 1.{"\n"}
$headline$: Use starting line of the note as title.{"\n"}
</Paragraph>
</>
);
};

View File

@@ -109,7 +109,7 @@ const SettingsUserSection = ({ item }) => {
flexGrow: 1
}}
>
<Heading color={colors.accent} size={SIZE.xs}>
<Heading color={colors.accent} size={SIZE.xs + 1}>
{SUBSCRIPTION_STATUS_STRINGS[
user.subscription?.type
]?.toUpperCase() || "Basic"}

View File

@@ -1,47 +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 InAppReview from "react-native-in-app-review";
import { DatabaseLogger } from "../common/database";
import { MMKV } from "../common/database/mmkv";
import Config from "react-native-config";
import { useUserStore } from "../stores/use-user-store";
const day_ms = 86400000;
export function requestInAppReview() {
if (Config.GITHUB_RELEASE === "true") return;
const time = MMKV.getMap<{ timestamp: number }>("requestInAppReview");
if (time?.timestamp && time?.timestamp + day_ms * 7 > Date.now()) {
return;
}
if (InAppReview.isAvailable()) {
useUserStore.getState().setShouldBlockVerifyUser(true);
InAppReview.RequestInAppReview()
.then(() => {})
.catch((error) => {
DatabaseLogger.error(error);
});
MMKV.setMap("requestInAppReview", { timestamp: Date.now() });
} else {
DatabaseLogger.error(new Error("In App Review not available"));
}
}

View File

@@ -21,7 +21,7 @@ import { Platform } from "react-native";
import FileViewer from "react-native-file-viewer";
import * as ScopedStorage from "react-native-scoped-storage";
import Share from "react-native-share";
import RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
import { presentDialog } from "../components/dialog/functions";
import { DatabaseLogger, db } from "../common/database";
import storage from "../common/database/storage";

View File

@@ -22,7 +22,7 @@ import { zipSync } from "fflate";
import { Platform } from "react-native";
import RNHTMLtoPDF from "react-native-html-to-pdf-lite";
import * as ScopedStorage from "react-native-scoped-storage";
import RNFetchBlob from "react-native-blob-util";
import RNFetchBlob from "rn-fetch-blob";
import { DatabaseLogger, db } from "../common/database/index";
import Storage from "../common/database/storage";
import { toTXT } from "../utils";

View File

@@ -45,7 +45,6 @@ import { sleep } from "../utils/time";
import { useRelationStore } from "../stores/use-relation-store";
import { useReminderStore } from "../stores/use-reminder-store";
import { presentDialog } from "../components/dialog/functions";
import NetInfo from "@react-native-community/netinfo";
export type Reminder = {
id: string;
@@ -194,7 +193,7 @@ const onEvent = async ({ type, detail }: Event) => {
case "Hide":
unpinQuickNote();
break;
case "ReplyInput": {
case "ReplyInput":
displayNotification({
title: "Quick note",
message: 'Tap on "Take note" to add a note.',
@@ -212,17 +211,9 @@ const onEvent = async ({ type, detail }: Event) => {
data: `<p>${input} </p>`
}
});
const status = await NetInfo.fetch();
if (status.isInternetReachable) {
try {
await db.sync(false, false);
} catch (e) {
console.log(e, (e as Error).stack);
}
}
await db.sync(false, false);
useNoteStore.getState().setNotes();
break;
}
}
}
};

View File

@@ -98,7 +98,7 @@ async function getProducts() {
}
function get() {
if (__DEV__ || Config.isTesting === "true") return true;
if (__DEV__ || Config.isTesting) return true;
return SUBSCRIPTION_STATUS.BASIC !== premiumStatus;
}
@@ -154,11 +154,9 @@ const onUserStatusCheck = async (type) => {
};
break;
case CHECK_IDS.notebookAdd:
message = {
context: "sheet",
title: "Get Notesnook Pro",
desc: "With Notesnook Pro you can create unlimited notebooks and do so much more! Get it now."
};
setTimeout(() => {
eSendEvent(eOpenPremiumDialog);
}, 500);
break;
case CHECK_IDS.vaultAdd:
message = {
@@ -179,6 +177,7 @@ const onUserStatusCheck = async (type) => {
const showVerifyEmailDialog = () => {
presentSheet({
title: "Confirm your email",
icon: "email",
paragraph:
"We have sent you an email confirmation link. Please check your email inbox. If you cannot find the email, check your spam folder.",
action: async () => {
@@ -368,7 +367,7 @@ const features_list = [
"Rich note editing experience with markdown, tables, checklists and more"
},
{
content: "Export your notes in PDF, markdown and html formats"
content: "Export your notes in Pdf, markdown and html formats"
}
];
@@ -387,7 +386,7 @@ const sheet = (context, promo, trial) => {
<Seperator />
<CompactFeatures
scrollRef={ref}
maxHeight={400}
maxHeight={300}
features={features_list}
vertical
/>

View File

@@ -36,12 +36,14 @@ export function validatePass(password) {
SHORT_PASS: false
};
if (password?.length < 8) {
if (password?.length <= 0) {
errors.SHORT_PASS = true;
} else {
errors.SHORT_PASS = false;
return errors;
}
if (password.length >= 8) {
errors.SHORT_PASS = false;
}
return errors;
}

View File

@@ -106,8 +106,6 @@ export interface SettingStore extends State {
setRequestBiometrics: (requestBiometrics: boolean) => void;
insets: Insets;
setInsets: (insets: Insets) => void;
timeFormat: string;
dateFormat: string;
}
const { width, height } = Dimensions.get("window");
@@ -172,8 +170,6 @@ export const useSettingStore = create<SettingStore>((set) => ({
requestBiometrics: false,
setRequestBiometrics: (requestBiometrics) => set({ requestBiometrics }),
setInsets: (insets) => set({ insets }),
timeFormat: "12-hour",
dateFormat: "DD-MM-YYYY",
insets: initialWindowMetrics?.insets
? initialWindowMetrics.insets
: { top: 0, right: 0, left: 0, bottom: 0 }

View File

@@ -37,8 +37,6 @@ export interface UserStore extends State {
setLastSynced: (lastSynced: string) => void;
verifyUser: boolean;
setVerifyUser: (verified: boolean) => void;
shouldBlockVerifyUser: boolean;
setShouldBlockVerifyUser: (shouldBlockVerifyUser: boolean) => void;
}
export const useUserStore = create<UserStore>((set) => ({
@@ -54,12 +52,5 @@ export const useUserStore = create<UserStore>((set) => ({
},
setLastSynced: (lastSynced) => set({ lastSynced: lastSynced }),
setVerifyUser: (verified) => set({ verifyUser: verified }),
lastSyncStatus: SyncStatus.Never,
shouldBlockVerifyUser: false,
setShouldBlockVerifyUser: (shouldBlockVerifyUser) => {
set({ shouldBlockVerifyUser });
setTimeout(() => {
set({ shouldBlockVerifyUser: false });
}, 1000);
}
lastSyncStatus: SyncStatus.Never
}));

Some files were not shown because too many files have changed in this diff Show More