Merge branch 'develop' into tiptap-editor

This commit is contained in:
ammarahm-ed
2022-03-25 09:40:01 +05:00
302 changed files with 12253 additions and 10045 deletions

1
apps/mobile/.env Normal file
View File

@@ -0,0 +1 @@
GITHUB_RELEASE=false

1
apps/mobile/.env.public Normal file
View File

@@ -0,0 +1 @@
GITHUB_RELEASE=true

View File

@@ -1,11 +1,17 @@
module.exports = {
parser: '@babel/eslint-parser',
parser: '@typescript-eslint/parser',
env: {
browser: true,
es2021: true,
'react-native/react-native': true
},
extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:prettier/recommended'],
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended'
],
parserOptions: {
ecmaFeatures: {
jsx: true
@@ -14,23 +20,19 @@ module.exports = {
es6: true,
sourceType: 'module'
},
plugins: ['react', 'react-native', 'prettier', 'unused-imports'],
plugins: ['react', 'react-native', 'prettier', 'unused-imports', '@typescript-eslint'],
rules: {
'react/display-name': 0,
'no-unused-vars': 'off',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_'
}
],
'react/no-unescaped-entities': 'off',
'unused-imports/no-unused-vars': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'prefer-const': 'off',
'no-empty': 'off',
'react/no-unescaped-entities': 'off',
'react/prop-types': 0,
'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }],
'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx', 'ts', 'tsx'] }],
'prettier/prettier': [
'error',
{},

View File

@@ -0,0 +1,71 @@
name: Android Github Release
on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v2
with:
persist-credentials: false
- uses: actions/setup-node@master
with:
node-version: '16'
- name: Use specific Java version for the builds
uses: joschi/setup-jdk@v2
with:
java-version: '11'
architecture: 'x64'
- name: Install node modules
run: |
npm install
- name: Make Gradlew Executable
run: cd android && chmod +x ./gradlew
- name: Build Android App APKS
run: |
cd android && ENVFILE=.env.public ./gradlew assembleRelease --no-daemon
- name: Sign APK Files
id: sign_app
uses: r0adkll/sign-android-release@master
with:
releaseDirectory: android/app/build/outputs/apk/release
signingKeyBase64: ${{ secrets.PUBLIC_SIGNING_KEY }}
alias: ${{ secrets.PUBLIC_ALIAS }}
keyStorePassword: ${{ secrets.PUBLIC_KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
- name: Get version
id: package-version
uses: martinbeentjes/npm-get-version-action@master
- name: Rename
run: |
cd android/app/build/outputs/apk/release/
mv app-arm64-v8a-release-unsigned-signed.apk notesnook-arm64-v8a.apk
mv app-armeabi-v7a-release-unsigned-signed.apk notesnook-armeabi-v7a.apk
mv app-x86-release-unsigned-signed.apk notesnook-x86.apk
mv app-x86_64-release-unsigned-signed.apk notesnook-x86_64.apk
- name: Publish
uses: softprops/action-gh-release@v1
with:
draft: true
tag_name: ${{ steps.package-version.outputs.current-version}}-android
name: Notesnook Android v${{ steps.package-version.outputs.current-version}}
repository: streetwriters/notesnook
token: ${{ secrets.NOTESNOOK_GH_TOKEN }}
files: |
android/app/build/outputs/apk/release/notesnook-arm64-v8a.apk
android/app/build/outputs/apk/release/notesnook-armeabi-v7a.apk
android/app/build/outputs/apk/release/notesnook-x86.apk
android/app/build/outputs/apk/release/notesnook-x86_64.apk

View File

@@ -2,6 +2,8 @@
#
.DS_Store
*Issues.md
# Xcode
#
build/

1
apps/mobile/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -1,107 +1,36 @@
import React, { useEffect } from 'react';
import Orientation from 'react-native-orientation';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import AppLoader from './src/components/AppLoader';
import { RootView } from './src/navigation/RootView';
import { useTracked } from './src/provider';
import { initialize, useSettingStore, useUserStore } from './src/provider/stores';
import { DDS } from './src/services/DeviceDetection';
import { eSendEvent, eSubscribeEvent, eUnSubscribeEvent } from './src/services/EventManager';
import Notifications from './src/services/Notifications';
import SettingsService from './src/services/SettingsService';
import { Tracker } from './src/utils';
import { db } from './src/utils/database';
import { eDispatchAction } from './src/utils/Events';
import { MMKV } from './src/utils/mmkv';
import { useAppEvents } from './src/utils/use-app-events';
import Launcher from './src/components/launcher';
import { ApplicationHolder } from './src/navigation';
import Notifications from './src/services/notifications';
import SettingsService from './src/services/settings';
import { TipManager } from './src/services/tip-manager';
import { useUserStore } from './src/stores/stores';
import { useAppEvents } from './src/utils/hooks/use-app-events';
let databaseHasLoaded = false;
const loadDatabase = async () => {
let requireIntro = await MMKV.getItem('introCompleted');
useSettingStore.getState().setIntroCompleted(requireIntro ? true : false);
await db.init();
Notifications.get();
await checkFirstLaunch();
};
async function checkFirstLaunch() {
let requireIntro = useSettingStore.getState().isIntroCompleted;
if (!requireIntro) {
await MMKV.setItem(
'askForRating',
JSON.stringify({
timestamp: Date.now() + 86400000 * 2
})
);
await MMKV.setItem(
'askForBackup',
JSON.stringify({
timestamp: Date.now() + 86400000 * 3
})
);
}
}
function checkOrientation() {
Orientation.getOrientation((e, r) => {
DDS.checkSmallTab(r);
useSettingStore.getState().setDimensions({ width: DDS.width, height: DDS.height });
useSettingStore
.getState()
.setDeviceMode(DDS.isLargeTablet() ? 'tablet' : DDS.isSmallTab ? 'smallTablet' : 'mobile');
});
}
const loadMainApp = () => {
if (databaseHasLoaded) {
SettingsService.setAppLoaded();
eSendEvent('load_overlay');
initialize();
}
};
checkOrientation();
SettingsService.checkOrientation();
const App = () => {
const [, dispatch] = useTracked();
const setVerifyUser = useUserStore(state => state.setVerifyUser);
const appEvents = useAppEvents();
useAppEvents();
useEffect(() => {
databaseHasLoaded = false;
(async () => {
try {
await SettingsService.init();
if (SettingsService.get().appLockMode && SettingsService.get().appLockMode !== 'none') {
setVerifyUser(true);
let appLockMode = SettingsService.get().appLockMode;
if (appLockMode && appLockMode !== 'none') {
useUserStore.getState().setVerifyUser(true);
}
await loadDatabase();
useUserStore.getState().setUser(await db.user.getUser());
if (SettingsService.get().telemetry) {
Tracker.record('3c6890ce-8410-49d5-8831-15fb2eb28a21');
}
} catch (e) {
} finally {
databaseHasLoaded = true;
loadMainApp();
}
await TipManager.init();
Notifications.get();
await SettingsService.onFirstLaunch();
} catch (e) {}
})();
}, []);
const _dispatch = data => {
dispatch(data);
};
useEffect(() => {
eSubscribeEvent(eDispatchAction, _dispatch);
return () => {
eUnSubscribeEvent(eDispatchAction, _dispatch);
};
}, []);
return (
<SafeAreaProvider>
<RootView />
<AppLoader onLoad={loadMainApp} />
<ApplicationHolder />
<Launcher />
</SafeAreaProvider>
);
};

View File

@@ -45,4 +45,6 @@
-keep class com.sun.jna.* { *; }
-keep class net.jpountz.** { *; }
-keep class com.goterl.** { *; }
-keepclassmembers class * extends com.sun.jna.* { public *; }
-keepclassmembers class * extends com.sun.jna.* { public *; }
-keep class com.streetwriters.notesnook.BuildConfig { *; }

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.streetwriters.notesnook">
<uses-permission android:name="android.permission.INTERNET" />
@@ -11,7 +12,9 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="remove"/>
<application
android:name=".MainApplication"
android:allowBackup="false"
@@ -22,7 +25,7 @@
android:theme="@style/BootTheme"
android:largeHeap="true"
android:usesCleartextTraffic="true">
<receiver android:exported="true" android:name=".NoteWidget">
<receiver android:exported="false" android:name=".NoteWidget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
@@ -73,6 +76,8 @@
android:noHistory="true"
android:screenOrientation="unspecified"
android:exported="true"
android:taskAffinity=""
android:excludeFromRecents="true"
android:windowSoftInputMode="adjustResize"
android:theme="@style/AppThemeB">
<intent-filter android:label="Make Note">
@@ -84,9 +89,7 @@
</intent-filter>
<intent-filter android:label="Make Note">
<action android:name="android.intent.action.PROCESS_TEXT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
</intent-filter>
</activity>

View File

@@ -16,14 +16,11 @@ public class NoteWidget extends AppWidgetProvider {
int appWidgetId) {
Intent intent = new Intent(context, ShareActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
;
CharSequence widgetText = context.getString(R.string.appwidget_text);
// Construct the RemoteViews object
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.note_widget);
views.setOnClickPendingIntent(R.id.widget_button, pendingIntent);
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views);
}

View File

@@ -1,5 +1,7 @@
- Fixed app hangs and becomes unusable on some phones
- Improved web clipper in widget to handle tables & codeblocks and remove invalid elements
- Disable swipe to close gesture in editor as it interferes with editing or scrolling.
- Two-factor authentication
- Fix collapse heading icon not showing correctly
- Fix items in search are not updated properly
- Improve login/signup UX
- Do not show progress on backup
Thank you for using Notesnook!

View File

@@ -1,4 +1,19 @@
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: ['transform-remove-console', '@babel/plugin-transform-named-capturing-groups-regex']
let env = process.env.BABEL_ENV;
const configs = {
env: {
development: {
presets: ['module:metro-react-native-babel-preset'],
plugins: ['@babel/plugin-transform-named-capturing-groups-regex']
},
production: {
presets: ['module:metro-react-native-babel-preset'],
plugins: ['transform-remove-console', '@babel/plugin-transform-named-capturing-groups-regex']
}
}
};
module.exports = function (api, opts) {
api.cache(true);
if (!env) env = 'production';
console.log('babel-env:', env);
return configs.env[env];
};

8
apps/mobile/features.ts Normal file
View File

@@ -0,0 +1,8 @@
import { FeatureType } from './src/components/sheets/new-feature';
export const features: FeatureType[] = [
{
title: 'Two-factor authentication',
body: 'Improved security for your account. Go to Settings to enable 2FA'
}
];

View File

@@ -26,15 +26,15 @@ const markdownPatterns = [
{ start: '*', end: '*', format: 'italic' },
{ start: '**', end: '**', format: 'bold' },
{ start: '`', end: '`', format: 'code' },
{ start: '#', format: 'h1' },
{ start: '##', format: 'h2' },
{ start: '###', format: 'h3' },
{ start: '####', format: 'h4' },
{ start: '#####', format: 'h5' },
{ start: '######', format: 'h6' },
{ start: '# ', format: 'h1' },
{ start: '## ', format: 'h2' },
{ start: '### ', format: 'h3' },
{ start: '#### ', format: 'h4' },
{ start: '##### ', format: 'h5' },
{ start: '###### ', format: 'h6' },
{ start: '* ', cmd: 'InsertUnorderedList' },
{ start: '- [x] ', cmd: 'insertCheckList', value: { checked: true } },
{ start: '- [ ] ', cmd: 'insertCheckList' },
{ start: '- [x] ', cmd: 'insertCheckList', value: 'checked' },
{ start: '- ', cmd: 'InsertUnorderedList' },
{ start: '> ', format: 'blockquote' },
{
@@ -319,7 +319,22 @@ function setTheme() {
color: white !important;
background: ${pageTheme.colors.accent} !important;
}
img::selection,
video::selection,
iframe::selection,
.mce-preview-object::selection {
color: white !important;
background: transparent !important;
} {
color: white !important;
background: transparent !important;
} {
color: white !important;
background: transparent !important;
}
.mce-content-body a[data-mce-selected] {
box-shadow: none !important;
@@ -408,12 +423,14 @@ span.attachment em::before {
font-family: ui-monospace, SFMono-Regular, SF Mono, Consolas, Liberation Mono,
Menlo, monospace !important;
font-size: 10pt !important;
overflow-x:auto !important;
}
.tox-checklist > li,
.checklist > li {
list-style: none;
margin: 0.25em 0;
color: ${pageTheme.colors.pri};
}
.tox-checklist > li::before,
@@ -440,7 +457,7 @@ span.attachment em::before {
.tox-checklist li.tox-checklist--checked,
.checklist li.checked {
color:${pageTheme.colors.icon}
color:${pageTheme.colors.icon};
}
[dir="rtl"] .tox-checklist > li::before,
@@ -600,6 +617,25 @@ pre code {
background-color: transparent !important;
font-size: 10pt !important;
padding: 0px 0px 0px 0px !important;
overflow-x:auto !important;
}
h1::before,
h2::before,
h3::before,
h4::before,
h5::before,
h6::before {
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' role='presentation' style='stroke-width: 0px; stroke: rgb(59, 59, 59); width: 14px; height: 14px;'%3E%3Cpath d='M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z' style='fill: ${
pageTheme.colors.icon
};'%3E%3C/path%3E%3C/svg%3E");
}
.c::before {
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' role='presentation' style='stroke-width: 0px; stroke: rgb(59, 59, 59); width: 14px; height: 14px;'%3E%3Cpath d='M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58' style='fill: ${
pageTheme.colors.icon
};'%3E%3C/path%3E%3C/svg%3E");
}
`;
@@ -1010,6 +1046,7 @@ table[data-mce-selected] th[data-mce-active] {
background-color: ${pageTheme.colors.nav} !important;
}
code:not(pre code),
.hljs {
background-color: ${pageTheme.colors.nav} !important;

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,6 @@ attachTitleInputListeners();
autosize();
function reactNativeEventHandler(type, value) {
if (window.ReactNativeWebView) {
console.log('type', type, 'id:', sessionId);
window.ReactNativeWebView.postMessage(
JSON.stringify({
type: type,
@@ -60,9 +59,6 @@ function loadFontSize() {
}
let changeTimer = null;
const COLLAPSED_KEY = 'c';
const HIDDEN_KEY = 'h';
const collapsibleTags = {HR: 1, H2: 2, H3: 3, H4: 4, H5: 5, H6: 6};
let styleElement;
function addStyle() {
@@ -75,46 +71,9 @@ function addStyle() {
body {
font-size:${DEFAULT_FONT_SIZE} !important;
}
.mce-content-body .c::before{
background-color:${pageTheme.colors.accent};
border-radius:3px;
color:white;
}
`;
}
function toggleElementVisibility(element, toggleState) {
if (!toggleState) element.classList.remove(HIDDEN_KEY);
else element.classList.add(HIDDEN_KEY);
}
function collapseElement(target) {
let sibling = target.nextSibling;
const isTargetCollapsed = target.classList.contains(COLLAPSED_KEY);
let skip = false;
while (
sibling &&
(!collapsibleTags[sibling.tagName] ||
collapsibleTags[sibling.tagName] > collapsibleTags[target.tagName])
) {
const isCollapsed = sibling.classList.contains(COLLAPSED_KEY);
if (!isTargetCollapsed) {
if (isCollapsed) {
skip = true;
toggleElementVisibility(sibling, isTargetCollapsed);
} else if (skip && collapsibleTags[sibling.tagName]) {
skip = false;
}
}
if (!skip) {
toggleElementVisibility(sibling, isTargetCollapsed);
}
addStyle();
if (!sibling.nextSibling) break;
sibling = sibling.nextSibling;
}
}
let undoTimer = null;
function onUndoChange() {
clearTimeout(undoTimer);
@@ -128,7 +87,6 @@ function onUndoChange() {
function init_callback(_editor) {
editor = _editor;
//console.log('init_call', editor);
setTheme();
editor.on('SelectionChange', function (e) {
@@ -142,87 +100,26 @@ function init_callback(_editor) {
editor.on('BeforeAddUndo', onUndoChange);
editor.on('AddUndo', onUndoChange);
editor.on('cut', function () {
onChange({type: 'cut'});
onChange({ type: 'cut' });
onUndoChange();
});
editor.on('copy', onUndoChange);
editor.on('paste', function () {
onChange({type: 'paste'});
onChange({ type: 'paste' });
});
editor.on('focus', function () {
reactNativeEventHandler('focus', 'editor');
});
// editor.on('SetContent', function (event) {
// if (globalThis.isClearingNoteData) {
// globalThis.isClearingNoteData = false;
// return;
// }
// setTimeout(function () {
// editor.undoManager.transact(function () {});
// }, 1000);
// if (!event.paste) {
// reactNativeEventHandler('noteLoaded', true);
// }
// });
editor.on('NewBlock', function (e) {
console.log('New Block', e);
const {newBlock} = e;
let target;
if (newBlock) {
target = newBlock.previousElementSibling;
}
if (target && target.classList.contains(COLLAPSED_KEY)) {
target.classList.remove(COLLAPSED_KEY);
collapseElement(target);
}
onChange(e);
});
editor.on('touchstart mousedown', function (e) {
const {target} = e;
if (
e.offsetX < 6 &&
collapsibleTags[target.tagName] &&
target.parentElement &&
target.parentElement.tagName === 'BODY'
) {
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
editor.undoManager.transact(function () {
if (target.classList.contains(COLLAPSED_KEY)) {
target.classList.remove(COLLAPSED_KEY);
} else {
target.classList.add(COLLAPSED_KEY);
}
collapseElement(target);
editor
.getHTML()
.then(function (html) {
reactNativeEventHandler('tiny', html);
})
.catch(function (e) {
reactNativeEventHandler('tinyerror', e.message);
});
});
}
});
editor.on('tap', function (e) {
if (
e.target.classList.contains('mce-content-body') &&
!e.target.innerText.length > 0
) {
if (e.target.classList.contains('mce-content-body') && !e.target.innerText.length > 0) {
e.preventDefault();
}
});
editor.on('ScrollIntoView', function (e) {
e.preventDefault();
console.log(e);
e.elm.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
@@ -231,7 +128,6 @@ function init_callback(_editor) {
editor.on('input ExecCommand ObjectResized Redo Undo ListMutation', onChange);
editor.on('keyup', function (e) {
console.log('keyup: ', e);
if (e.key !== 'Backspace') return;
onChange(e);
});
@@ -239,7 +135,7 @@ function init_callback(_editor) {
const plugins = [
'checklist advlist autolink textpattern hr lists link noneditable image bettertable',
'searchreplace codeblock inlinecode keyboardquirks attachmentshandler',
'searchreplace codeblock inlinecode keyboardquirks attachmentshandler collapsibleheaders',
'media imagetools table paste wordcount autoresize directionality blockescape contenthandler'
];
@@ -259,41 +155,7 @@ body {
${margins}
}
.mce-content-body h2::before,
h3::before,
h4::before,
h5::before,
h6::before {
font-size: 11px;
font-weight: normal;
letter-spacing: 1.1px;
padding: 1px 3px 1px 3px;
margin-left: -12px;
margin-right: 5px;
cursor: row-resize;
margin-top:-3px;
vertical-align:middle;
}
.mce-content-body h2::before {
content: "H2";
}
.mce-content-body h3::before {
content: "H3";
}
.mce-content-body h4::before {
content: "H4";
}
.mce-content-body h5::before {
content: "H5";
}
.h {
display: none;
}
span.diff-del {
background-color: #FDB0C0;
}
@@ -303,11 +165,15 @@ span.diff-ins {
pre.codeblock {
overflow-x:auto;
}
img {
img,
video {
max-width:100% !important;
height:auto !important;
border-radius:5px !important;
margin-bottom:10px;
}
.tox .tox-edit-area__iframe {
background-color:transparent !important;
}
@@ -320,9 +186,12 @@ body {
background-color:transparent !important;
font-size:${DEFAULT_FONT_SIZE}
}
.mce-preview-object,
iframe {
max-width:100% !important;
background-color:transparent !important;
height:auto !important;
border-radius:5px !important;
}
h1,
@@ -333,6 +202,54 @@ h5,
h6,
strong {
font-weight:600 !important;
position:relative;
padding-left:10px;
}
h1::before,
h2::before,
h3::before,
h4::before,
h5::before,
h6::before {
opacity: 1;
cursor: row-resize;
margin-right: 7px;
margin-left: -15px;
width: 24px;
height: 24px;
display: inline-block;
border: none;
position:absolute;
margin-left: -25px;
}
h1::before {
top:2px;
}
h2::before {
top:5px;
}
h3::before {
top:2px;
}
h4::before {
top:0px;
}
h5::before {
top:0px;
width: 20px;
height: 20px;
}
h6::before {
top:-1px;
width: 18px;
height: 18px;
}
.h {
display: none !important;
}
`;
@@ -354,7 +271,6 @@ function tableCellNodeOptions() {
function findNodeParent(nodeName) {
let node = editor.selection.getNode();
console.log('finding node', node);
let levels = 5;
for (let i = 0; i < levels; i++) {
if (!node) return;
@@ -381,7 +297,6 @@ function tableRowNodeOptions() {
function init_tiny(size) {
loadFontSize();
console.log('init tinymce');
tinymce.init({
selector: '#tiny_textarea',
menubar: false,
@@ -409,8 +324,7 @@ function init_tiny(size) {
autoresize_bottom_margin: 120,
table_toolbar:
'tcellprops trowprops | tableinsertrowafter tableinsertcolafter tabledeleterow tabledeletecol | tableconfig',
imagetools_toolbar:
'imagedownload | rotateleft rotateright flipv fliph | imageopts ',
imagetools_toolbar: 'imagedownload | rotateleft rotateright flipv fliph | imageopts ',
placeholder: 'Start writing your note here',
object_resizing: true,
table_responsive_width: false,
@@ -499,7 +413,6 @@ function setup_tiny(_editor) {
icon: 'more-drawer',
tooltip: 'Table properties',
onAction: function (e) {
console.log(e, 'event');
reactNativeEventHandler('tableconfig');
}
});
@@ -558,10 +471,7 @@ function setup_tiny(_editor) {
};
reader.readAsDataURL(recoveredBlob);
};
xhr.open(
'GET',
tinymce.activeEditor.selection.getNode().getAttribute('src')
);
xhr.open('GET', tinymce.activeEditor.selection.getNode().getAttribute('src'));
xhr.send();
}
}
@@ -586,13 +496,11 @@ function scrollSelectionIntoView(event) {
event.data &&
event.data.endsWith('\n')
) {
console.log(event);
clearTimeout(inputKeyTimer);
inputKeyTimer = setTimeout(function () {
let node = editor.selection.getNode();
if (node) {
console.log(node, 'scrolling into view');
node.scrollIntoView({behavior: 'smooth', block: 'nearest'});
node.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}, 1);
}
@@ -622,7 +530,6 @@ const onChange = function (event) {
}
if (prevCount === 0 && event.type !== 'paste') return;
console.log(event);
if (event.type !== 'compositionend') {
if (!noteedited) {
noteedited = true;
@@ -639,7 +546,10 @@ const onChange = function (event) {
reactNativeEventHandler('tiny', html);
})
.catch(function (e) {
reactNativeEventHandler('tinyerror', e.message);
reactNativeEventHandler('tinyerror', {
message: e?.message,
stack: e?.stack
});
});
onUndoChange();
@@ -652,14 +562,13 @@ function updateCount(timer = 1000) {
countTimer = null;
if (!timer) {
let count = editor.countWords();
let count = editor.countWords() || 0;
info = document.querySelector('.info-bar');
info.querySelector('#infowords').innerText = count + ' words';
prevCount = count;
console.log('timer', 'updating here');
} else {
countTimer = setTimeout(function () {
let count = editor.countWords();
let count = editor.countWords() | 0;
info = document.querySelector('.info-bar');
info.querySelector('#infowords').innerText = count + ' words';
prevCount = count;

View File

@@ -74,7 +74,8 @@ function onTitleChange() {
info = document.querySelector(infoBar);
if (tinymce.activeEditor) {
info.querySelector('#infowords').innerText = editor.countWords() + ' words';
let count = editor.countWords() || 0;
info.querySelector('#infowords').innerText = count + ' words';
updateInfoBar();
}
@@ -153,8 +154,9 @@ function attachMessageListener() {
isLoading = true;
globalThis.isClearingNoteData = false;
tinymce.activeEditor.mode.set('readonly');
if (!isInvalidValue(value)) {
tinymce.activeEditor.setHTML(value);
let html = value.data;
if (!isInvalidValue(html)) {
tinymce.activeEditor.setHTML(html);
let timeout = 0;
if (value.length > 400000) {
timeout = 900;
@@ -178,9 +180,19 @@ function attachMessageListener() {
globalThis.isClearingNoteData = false;
reactNativeEventHandler('noteLoaded', true);
}
tinymce.activeEditor.mode.set('design');
if (!value.readOnly) {
tinymce.activeEditor.mode.set('design');
document.getElementById('titleInput').readOnly = false;
} else {
tinymce.activeEditor.mode.set('design');
tinymce.activeEditor.mode.set('readonly');
document.getElementById('titleInput').readOnly = true;
}
info = document.querySelector(infoBar);
info.querySelector('#infowords').innerText = editor.countWords() + ' words';
let count = editor.countWords() || 0;
info.querySelector('#infowords').innerText = count + ' words';
updateInfoBar();
break;
case 'htmldiff':

View File

@@ -21,7 +21,7 @@ index a240d41..e8b38f4 100644
'table-merge-cells': '<svg width="24" height="24"><path fill-rule="nonzero" d="M19 4a2 2 0 012 2v12a2 2 0 01-2 2H5a2 2 0 01-2-2V6c0-1.1.9-2 2-2h14zM5 15.5V18h3v-2.5H5zm14-5h-9V18h9v-7.5zM19 6h-4v2.5h4V6zM8 6H5v2.5h3V6zm5 0h-3v2.5h3V6zm-8 7.5h3v-3H5v3z"/></svg>',
'table-row-numbering-rtl': '<svg width="24" height="24"><path d="M6 4a2 2 0 00-2 2v13c0 1.1.9 2 2 2h12a2 2 0 002-2V6a2 2 0 00-2-2H6zm0 12h8v3H6v-3zm11 0c.6 0 1 .4 1 1v1a1 1 0 01-2 0v-1c0-.6.4-1 1-1zM6 11h8v3H6v-3zm11 0c.6 0 1 .4 1 1v1a1 1 0 01-2 0v-1c0-.6.4-1 1-1zM6 6h8v3H6V6zm11 0c.6 0 1 .4 1 1v1a1 1 0 11-2 0V7c0-.6.4-1 1-1z"/></svg>',
diff --git a/node_modules/tinymce/plugins/table/plugin.js b/node_modules/tinymce/plugins/table/plugin.js
index e41249b..fb25fc5 100644
index c707290..4e49eb6 100644
--- a/node_modules/tinymce/plugins/table/plugin.js
+++ b/node_modules/tinymce/plugins/table/plugin.js
@@ -1875,7 +1875,8 @@
@@ -53,25 +53,8 @@ index e41249b..fb25fc5 100644
];
var handle$1 = function (event, editor, cellSelection) {
if (event.keyCode === global$1.TAB) {
diff --git a/node_modules/tinymce/tinymce.js b/node_modules/tinymce/tinymce.js
index 4cdbfce..464b5fd 100644
--- a/node_modules/tinymce/tinymce.js
+++ b/node_modules/tinymce/tinymce.js
@@ -27901,10 +27901,10 @@
setEditorCommandState(editor, 'StyleWithCSS', false);
setEditorCommandState(editor, 'enableInlineTableEditing', false);
setEditorCommandState(editor, 'enableObjectResizing', false);
- if (hasEditorOrUiFocus(editor)) {
- editor.focus();
- }
- restoreFakeSelection(editor);
+ // if (hasEditorOrUiFocus(editor)) {
+ //editor.focus();
+ // }
+ //restoreFakeSelection(editor);
editor.nodeChanged();
}
};
diff --git a/node_modules/tinymce/plugins/textpattern/plugin.js b/node_modules/tinymce/plugins/textpattern/plugin.js
index 05ccbd1..cc3fb87 100644
--- a/node_modules/tinymce/plugins/textpattern/plugin.js
+++ b/node_modules/tinymce/plugins/textpattern/plugin.js
@@ -1330,6 +1330,12 @@
@@ -87,7 +70,7 @@ index 4cdbfce..464b5fd 100644
var setup = function (editor, patternsState) {
var charCodes = [
',',
@@ -1341,6 +1347,8 @@
@@ -1341,18 +1347,44 @@
];
var keyCodes = [32];
editor.on('keydown', function (e) {
@@ -96,9 +79,29 @@ index 4cdbfce..464b5fd 100644
if (e.keyCode === 13 && !global$3.modifierPressed(e)) {
if (handleEnter(editor, patternsState.get())) {
e.preventDefault();
@@ -1348,11 +1356,15 @@
}
}
}, true);
+
+
+ editor.on('input', function (e) {
+ if (isCodeblock(editor)) return;
+
+ if (e.data && e.data.endsWith("\n") && !global$3.modifierPressed(e)) {
+ if (handleEnter(editor, patternsState.get())) {
+ e.preventDefault();
+ }
+ }
+
+ if (e.data && e.data.endsWith(" ")) {
+ e.preventDefault();
+ global$4.setEditorTimeout(editor, function () {
+ handleInlineKey(editor, patternsState.get());
+ });
+ }
+
+ }, true);
+
editor.on('keyup', function (e) {
+ if (isCodeblock(editor)) return;
+
@@ -108,13 +111,15 @@ index 4cdbfce..464b5fd 100644
});
editor.on('keypress', function (e) {
+ if (isCodeblock(editor)) return;
+
+
if (checkCharCode(charCodes, e)) {
global$4.setEditorTimeout(editor, function () {
handleInlineKey(editor, patternsState.get());
handleInlineKey(editor, patternsState.get());
diff --git a/node_modules/tinymce/tinymce.js b/node_modules/tinymce/tinymce.js
index 755049c..807bc93 100644
--- a/node_modules/tinymce/tinymce.js
+++ b/node_modules/tinymce/tinymce.js
@@ -24894,6 +24894,9 @@
@@ -24897,6 +24897,9 @@
var inPreBlock = function (requiredState) {
return inBlock('pre', requiredState);
};
@@ -124,7 +129,7 @@ index 4cdbfce..464b5fd 100644
var inSummaryBlock = function () {
return inBlock('summary', true);
};
@@ -24925,6 +24928,13 @@
@@ -24928,6 +24931,13 @@
return evaluateUntil([
match([shouldBlockNewLine], newLineAction.none()),
match([inSummaryBlock()], newLineAction.br()),
@@ -137,4 +142,19 @@ index 4cdbfce..464b5fd 100644
+ ], newLineAction.br()),
match([
inPreBlock(true),
shouldPutBrInPre(false),
shouldPutBrInPre(false),
@@ -27904,10 +27914,10 @@
setEditorCommandState(editor, 'StyleWithCSS', false);
setEditorCommandState(editor, 'enableInlineTableEditing', false);
setEditorCommandState(editor, 'enableObjectResizing', false);
- if (hasEditorOrUiFocus(editor)) {
- editor.focus();
- }
- restoreFakeSelection(editor);
+ // if (hasEditorOrUiFocus(editor)) {
+ //editor.focus();
+ // }
+ //restoreFakeSelection(editor);
editor.nodeChanged();
}
};

View File

@@ -26,6 +26,7 @@ import '@streetwriters/tinymce-plugins/attachmentshandler';
import '@streetwriters/tinymce-plugins/blockescape';
import '@streetwriters/tinymce-plugins/contenthandler';
import '@streetwriters/tinymce-plugins/bettertable';
import '@streetwriters/tinymce-plugins/collapsibleheaders';
require.context(
'file-loader?name=[path][name].[ext]&context=node_modules/tinymce!tinymce/skins',

View File

@@ -1,10 +1,10 @@
import React from 'react';
import { AppRegistry, LayoutAnimation, LogBox, Platform, UIManager } from 'react-native';
import { AppRegistry, LogBox, Platform, UIManager } from 'react-native';
import 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { enableScreens } from 'react-native-screens';
import { name as appName } from './app.json';
import Notifications from './src/services/Notifications';
import Notifications from './src/services/notifications';
global.Buffer = require('buffer').Buffer;
enableScreens(true);
UIManager.setLayoutAnimationEnabledExperimental &&
@@ -14,20 +14,14 @@ if (__DEV__) {
LogBox.ignoreAllLogs();
}
let Provider;
let App;
let NotesnookShare;
Notifications.init();
let QuickNoteIOS;
const AppProvider = () => {
Provider = require('./src/provider').Provider;
App = require('./App').default;
return (
<Provider>
<App />
</Provider>
);
return <App />;
};
AppRegistry.registerComponent(appName, () => AppProvider);

View File

@@ -1118,7 +1118,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1714;
CURRENT_PROJECT_VERSION = 1804;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
GCC_GENERATE_DEBUGGING_SYMBOLS = YES;
@@ -1191,7 +1191,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.7.13;
MARKETING_VERSION = 1.8.4;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1219,7 +1219,7 @@
CODE_SIGN_ENTITLEMENTS = Notesnook/Notesnook.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1714;
CURRENT_PROJECT_VERSION = 1804;
DEVELOPMENT_TEAM = 53CWBG3QUC;
GCC_GENERATE_DEBUGGING_SYMBOLS = YES;
HEADER_SEARCH_PATHS = (
@@ -1291,7 +1291,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.7.13;
MARKETING_VERSION = 1.8.4;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1449,7 +1449,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1714;
CURRENT_PROJECT_VERSION = 1804;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -1460,7 +1460,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.7.13;
MARKETING_VERSION = 1.8.4;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1490,7 +1490,7 @@
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1714;
CURRENT_PROJECT_VERSION = 1804;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 53CWBG3QUC;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -1501,7 +1501,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.7.13;
MARKETING_VERSION = 1.8.4;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1530,7 +1530,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1714;
CURRENT_PROJECT_VERSION = 1804;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -1603,7 +1603,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.7.13;
MARKETING_VERSION = 1.8.4;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1633,7 +1633,7 @@
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1714;
CURRENT_PROJECT_VERSION = 1804;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 53CWBG3QUC;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -1706,7 +1706,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.7.13;
MARKETING_VERSION = 1.8.4;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -4,7 +4,7 @@ require_relative '../node_modules/@react-native-community/cli-platform-ios/nativ
platform :ios, '11.0'
pod 'Base64'
pod 'SexyTooltip',:git => 'https://github.com/prscX/SexyTooltip.git'
pod 'SexyTooltip',:git => 'https://github.com/ammarahm-ed/SexyTooltip.git'
pod 'MMKV'
target 'Notesnook' do

View File

@@ -237,6 +237,10 @@ PODS:
- React-Core
- react-native-begin-background-task (0.1.0):
- React
- react-native-config (1.4.5):
- react-native-config/App (= 1.4.5)
- react-native-config/App (1.4.5):
- React-Core
- react-native-document-picker (7.1.3):
- React-Core
- react-native-fingerprint-scanner (5.0.0):
@@ -407,6 +411,7 @@ DEPENDENCIES:
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- react-native-background-actions (from `../node_modules/react-native-background-actions`)
- react-native-begin-background-task (from `../node_modules/react-native-begin-background-task`)
- react-native-config (from `../node_modules/react-native-config`)
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
- react-native-fingerprint-scanner (from `../node_modules/react-native-fingerprint-scanner`)
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
@@ -453,7 +458,7 @@ DEPENDENCIES:
- RNSVG (from `../node_modules/react-native-svg`)
- RNTooltips (from `../node_modules/react-native-tooltips`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- SexyTooltip (from `https://github.com/prscX/SexyTooltip.git`)
- SexyTooltip (from `https://github.com/ammarahm-ed/SexyTooltip.git`)
- "toolbar-android (from `../node_modules/@react-native-community/toolbar-android`)"
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -508,6 +513,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-background-actions"
react-native-begin-background-task:
:path: "../node_modules/react-native-begin-background-task"
react-native-config:
:path: "../node_modules/react-native-config"
react-native-document-picker:
:path: "../node_modules/react-native-document-picker"
react-native-fingerprint-scanner:
@@ -601,7 +608,7 @@ EXTERNAL SOURCES:
RNVectorIcons:
:path: "../node_modules/react-native-vector-icons"
SexyTooltip:
:git: https://github.com/prscX/SexyTooltip.git
:git: https://github.com/ammarahm-ed/SexyTooltip.git
toolbar-android:
:path: "../node_modules/@react-native-community/toolbar-android"
Yoga:
@@ -609,8 +616,8 @@ EXTERNAL SOURCES:
CHECKOUT OPTIONS:
SexyTooltip:
:commit: 2fbe10260fb1b0f7dcd9822681a6970ce88112af
:git: https://github.com/prscX/SexyTooltip.git
:commit: bb13cf11a7c19b635f10047fc04d857dd6cb6553
:git: https://github.com/ammarahm-ed/SexyTooltip.git
SPEC CHECKSUMS:
Base64: cecfb41a004124895a7bcee567a89bae5a89d49b
@@ -640,6 +647,7 @@ SPEC CHECKSUMS:
React-logger: b08f354e4c928ff794ca477347fea0922aaf11c3
react-native-background-actions: a466fde06b54e52adf037ca9b217df460596f120
react-native-begin-background-task: 3b889e07458afc5822a7277cf9cbc7cd077e39ee
react-native-config: 6502b1879f97ed5ac570a029961fc35ea606cd14
react-native-document-picker: ec07866a30707f23660c0f3ae591d669d3e89096
react-native-fingerprint-scanner: be63e626b31fb951780a5fac5328b065a61a3d6e
react-native-get-random-values: 30b3f74ca34e30e2e480de48e4add2706a40ac8f
@@ -684,12 +692,12 @@ SPEC CHECKSUMS:
RNSecureRandom: 0dcee021fdb3d50cd5cee5db0ebf583c42f5af0e
RNShare: 3185c074441b7e8897014d95ba982434a0a024a1
RNSVG: 551acb6562324b1d52a4e0758f7ca0ec234e278f
RNTooltips: 47a992eb7b12f624e5a8a40f0990fbef49dcb83e
RNTooltips: 5424d4bf0b3d441104127943b1115cc7f0616b1f
RNVectorIcons: 4143ba35feebab8fdbe6bc43d1e776b393d47ac8
SexyTooltip: 5c9b4dec52bfb317938cb0488efd9da3717bb6fd
toolbar-android: 2a73856e98b750d7e71ce4644d3f41cc98211719
Yoga: 5cbf25add73edb290e1067017690f7ebf56c5468
PODFILE CHECKSUM: 69a787dbce9f45b28a09e255a4ae39a9cfe82d20
PODFILE CHECKSUM: caa8e050bad1a0f4d452e7e74a6dc724578310b1
COCOAPODS: 1.11.2

View File

@@ -1,8 +1,8 @@
diff --git a/node_modules/react-native-tooltips/ios/RNTooltips.podspec b/node_modules/react-native-tooltips/RNTooltips.podspec
similarity index 91%
similarity index 87%
rename from node_modules/react-native-tooltips/ios/RNTooltips.podspec
rename to node_modules/react-native-tooltips/RNTooltips.podspec
index 0ccfd23..9cd8105 100644
index 0ccfd23..7771e6f 100644
--- a/node_modules/react-native-tooltips/ios/RNTooltips.podspec
+++ b/node_modules/react-native-tooltips/RNTooltips.podspec
@@ -1,6 +1,6 @@
@@ -13,6 +13,14 @@ index 0ccfd23..9cd8105 100644
Pod::Spec.new do |s|
s.name = 'RNTooltips'
@@ -13,7 +13,6 @@ Pod::Spec.new do |s|
s.source = { :git => 'https://github.com/prscX/react-native-tooltips.git', :tag => s.version }
s.platform = :ios, '9.0'
- s.ios.deployment_target = '8.0'
s.preserve_paths = 'LICENSE', 'package.json'
s.source_files = '**/*.{h,m}'
diff --git a/node_modules/react-native-tooltips/android/build.gradle b/node_modules/react-native-tooltips/android/build.gradle
index 978045f..fdb9c22 100644
--- a/node_modules/react-native-tooltips/android/build.gradle
@@ -30503,3 +30511,220 @@ index 0000000..723ad7a
+ px.tooltips.RNTooltipsModule$2
+px/tooltips/RNTooltipsPackage.java
+ px.tooltips.RNTooltipsPackage
diff --git a/node_modules/react-native-tooltips/ios/RNTooltips.h b/node_modules/react-native-tooltips/ios/RNTooltips.h
index a150e3e..096ad86 100644
--- a/node_modules/react-native-tooltips/ios/RNTooltips.h
+++ b/node_modules/react-native-tooltips/ios/RNTooltips.h
@@ -1,6 +1,11 @@
#import "RCTUIManager.h"
#import <UIKit/UIKit.h>
+#if __has_include("RCTBridgeModule.h")
+#import "RCTBridgeModule.h"
+#else
+#import <React/RCTBridgeModule.h>
+#endif
#import <SexyTooltip/SexyTooltip.h>
diff --git a/node_modules/react-native-tooltips/ios/RNTooltips.m b/node_modules/react-native-tooltips/ios/RNTooltips.m
index fb96466..e1f3d9a 100644
--- a/node_modules/react-native-tooltips/ios/RNTooltips.m
+++ b/node_modules/react-native-tooltips/ios/RNTooltips.m
@@ -1,4 +1,6 @@
#import "RNTooltips.h"
+#import "RCTUIManager.h"
+#import "RCTUIManagerUtils.h"
@interface TooltipDelegate : NSObject <SexyTooltipDelegate>
@@ -7,9 +9,15 @@ @interface TooltipDelegate : NSObject <SexyTooltipDelegate>
@end
@implementation TooltipDelegate
+bool didHide = NO;
+
- (void)tooltipDidDismiss:(SexyTooltip *)tooltip {
+ if (didHide == YES) return;
+ tooltip.delegate = nil;
tooltip = nil;
+ didHide = YES;
_onHide(@[]);
+
}
@end
@@ -17,77 +25,120 @@ - (void)tooltipDidDismiss:(SexyTooltip *)tooltip {
@implementation RNTooltips
-SexyTooltip *toolTip;
+NSMutableDictionary *tooltips;
@synthesize bridge = _bridge;
+- (instancetype)init
+{
+ self = [super init];
+ tooltips = [NSMutableDictionary dictionary];
+ return self;
+}
+
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
RCT_EXPORT_MODULE()
-
RCT_EXPORT_METHOD(Show:(nonnull NSNumber *)targetViewTag inParentView:(nonnull NSNumber *)parentViewTag props:(NSDictionary *)props onHide:(RCTResponseSenderBlock)onHide)
{
- UIView *target = [self.bridge.uiManager viewForReactTag: targetViewTag];
- UIView *parentView = [self.bridge.uiManager viewForReactTag:parentViewTag];
- if (!parentView) {
- // parent is null, then return
- return;
- }
-
- NSString *text = [props objectForKey: @"text"];
-// NSNumber *position = [props objectForKey: @"position"]; // not used yet
-// NSNumber *align = [props objectForKey: @"align"]; // not used yet
- NSNumber *autoHide = [props objectForKey: @"autoHide"];
- NSNumber *duration = [props objectForKey: @"duration"];
- NSNumber *clickToHide = [props objectForKey: @"clickToHide"];
- NSNumber *corner = [props objectForKey: @"corner"];
- NSString *tintColor = [props objectForKey: @"tintColor"];
- NSString *textColor = [props objectForKey: @"textColor"];
- NSNumber *textSize = [props objectForKey: @"textSize"];
-// NSNumber *gravity = [props objectForKey: @"gravity"]; not used yet
- NSNumber *shadow = [props objectForKey: @"shadow"];
- NSNumber *arrow = [props objectForKey: @"arrow"];
-
- NSMutableAttributedString *attributes = [[NSMutableAttributedString alloc] initWithString: text];
- [attributes addAttribute:NSForegroundColorAttributeName value:[RNTooltips colorFromHexCode: textColor] range:NSMakeRange(0, text.length)];
- [attributes addAttribute:NSFontAttributeName value: [UIFont systemFontOfSize: [textSize floatValue]] range:NSMakeRange(0,text.length)];
-
- toolTip = [[SexyTooltip alloc] initWithAttributedString: attributes sizedToView:parentView];
- toolTip.layer.zPosition = 9999; // make sure the tooltips is always in front of other views.
-
- TooltipDelegate *delegate = [[TooltipDelegate alloc] init];
- delegate.onHide = onHide;
- [toolTip setDelegate: delegate];
-
- toolTip.color = [RNTooltips colorFromHexCode: tintColor];
- toolTip.cornerRadius = [corner floatValue];
- toolTip.dismissesOnTap = [clickToHide boolValue];
- toolTip.padding = UIEdgeInsetsMake(6.0, 8.0, 6.0, 8.0);
-
- if (![arrow boolValue]) {
- toolTip.arrowHeight = 0;
- }
- if ([shadow boolValue]) {
- toolTip.hasShadow = YES;
- }
- if ([autoHide boolValue]) {
- [toolTip dismissInTimeInterval:(NSTimeInterval) [duration floatValue] animated: YES];
+ UIView *target = [self.bridge.uiManager viewForReactTag: targetViewTag];
+ UIView *parentView = [self.bridge.uiManager viewForReactTag:parentViewTag];
+ if (!parentView) {
+ return;
+ }
+ NSString *text = props[@"text"];
+ NSNumber *position = props[@"position"];
+ // NSNumber *align = [props objectForKey: @"align"]; // not used yet
+ NSNumber *autoHide = props[@"autoHide"];
+ NSNumber *duration = props[@"duration"];
+ NSNumber *clickToHide = props[@"clickToHide"];
+ NSNumber *corner = props[@"corner"];
+ NSString *tintColor = props[@"tintColor"];
+ NSString *textColor = props[@"textColor"];
+ NSNumber *textSize = props[@"textSize"];
+ // NSNumber *gravity = [props objectForKey: @"gravity"]; not used yet
+ NSNumber *shadow = props[@"shadow"];
+ NSNumber *arrow = props[@"arrow"];
+
+ NSMutableAttributedString *attributes = [[NSMutableAttributedString alloc] initWithString: text];
+ [attributes addAttribute:NSForegroundColorAttributeName value:[RNTooltips colorFromHexCode: textColor] range:NSMakeRange(0, text.length)];
+ [attributes addAttribute:NSFontAttributeName value: [UIFont systemFontOfSize: [textSize floatValue]] range:NSMakeRange(0,text.length)];
+
+ SexyTooltip *toolTip = [[SexyTooltip alloc] initWithAttributedString: attributes sizedToView:parentView];
+
+ if (position != nil) {
+ [toolTip setPermittedArrowDirections:@[@([self getPosition:position])]];
+ [toolTip setArrowDirection:[self getPosition:position]];
+ }
+
+ TooltipDelegate *delegate = [[TooltipDelegate alloc] init];
+ delegate.onHide = onHide;
+ [toolTip setDelegate: delegate];
+ toolTip.color = [RNTooltips colorFromHexCode: tintColor];
+ toolTip.cornerRadius = [corner floatValue];
+ toolTip.dismissesOnTap = [clickToHide boolValue];
+ toolTip.padding = UIEdgeInsetsMake(6.0, 8.0, 6.0, 8.0);
+
+ if (![arrow boolValue]) {
+ toolTip.arrowHeight = 0;
+ }
+ if ([shadow boolValue]) {
+ toolTip.hasShadow = YES;
+ }
+ if ([autoHide boolValue]) {
+ [self dismissAfterDelay:targetViewTag.stringValue duration:duration];
+ }
+
+ [toolTip presentFromView:target animated:YES];
+ [parentView.superview bringSubviewToFront:toolTip];
+ [tooltips setObject:toolTip forKey:targetViewTag.stringValue];
+}
+
+- (SexyTooltipArrowDirection) getPosition:(NSNumber *)position {
+ if (position.intValue == 1) {
+ return SexyTooltipArrowDirectionRight;
+ } else if (position.intValue == 2) {
+ return SexyTooltipArrowDirectionLeft;
+ } else if (position.intValue == 3) {
+ return SexyTooltipArrowDirectionDown;
+ } else if (position.intValue == 4) {
+ return SexyTooltipArrowDirectionUp;
+ } else {
+ return SexyTooltipArrowDirectionDown;
}
- [toolTip presentFromView:target animated:YES];
+}
+
+- (void) dismissAfterDelay:(NSString*)viewTag duration:(NSNumber *)duration {
+
+ float delay = [duration floatValue]/1000;
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC*delay),
+ dispatch_get_main_queue(), ^{
+ if (tooltips[viewTag] == nil) {
+ return;
+ }
+ SexyTooltip *tooltip = tooltips[viewTag];
+ [tooltip dismissAnimated:true];
+ tooltip.delegate = nil;
+ tooltip = nil;
+ [tooltips removeObjectForKey:viewTag];
+ });
}
RCT_EXPORT_METHOD(Dismiss:(nonnull NSNumber *)view) {
- if (toolTip == nil) {
- return;
- }
+ if (tooltips[view.stringValue] == nil) {
+ return;
+ }
+
+ SexyTooltip *tooltip = tooltips[view.stringValue];
+ [tooltip dismiss];
+ tooltip = nil;
+ [tooltips removeObjectForKey:view.stringValue];
- [toolTip dismiss];
- toolTip = nil;
}

View File

@@ -1,5 +1,8 @@
- Fixed app hangs and becomes unusable on some phones
- Improved web clipper in widget to handle tables & codeblocks and remove invalid elements
- Disable swipe to close gesture in editor as it interferes with editing or scrolling.
- Two-factor authentication
- Fix collapse heading icon not showing correctly
- Fix items in search are not updated properly
- Improve login/signup UX
- Do not show progress on backup
Thank you for using Notesnook!

View File

@@ -0,0 +1,11 @@
# What's new
- Two-factor authentication
# What's fixed
- Fix collapse heading icon not showing correctly
- Fix items in search are not updated properly
- Improve login/signup UX
- Do not show progress on backup
Thank you for using Notesnook!

View File

@@ -25,11 +25,11 @@ import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import WebView from 'react-native-webview';
import ShareExtension from 'rn-extensions-share';
import isURL from 'validator/lib/isURL';
import { eSendEvent, eSubscribeEvent, eUnSubscribeEvent } from '../src/services/EventManager';
import { eSendEvent, eSubscribeEvent, eUnSubscribeEvent } from '../src/services/event-manager';
import { getElevation } from '../src/utils';
import { db } from '../src/utils/database';
import Storage from '../src/utils/storage';
import { sleep } from '../src/utils/TimeUtils';
import Storage from '../src/utils/database/storage';
import { sleep } from '../src/utils/time';
import { Search } from './search';
import { useShareStore } from './store';
@@ -362,7 +362,7 @@ const NotesnookShare = ({ quicknote = false }) => {
const onPress = async () => {
let content = await getContent();
if (!content || content === '') {
if (!content || content === '' || typeof content !== 'string') {
return;
}
setLoading(true);

View File

@@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { Appearance, SafeAreaView } from 'react-native';
import RNBootSplash from 'react-native-bootsplash';
import { COLOR_SCHEME_DARK, COLOR_SCHEME_LIGHT } from '../src/utils/Colors';
import { COLOR_SCHEME_DARK, COLOR_SCHEME_LIGHT } from '../src/utils/color-scheme';
import NotesnookShare from './index';
export default class QuickNoteIOS extends Component {

View File

@@ -1,17 +1,21 @@
import { Appearance } from 'react-native';
import create from 'zustand';
import { ACCENT, COLOR_SCHEME_DARK, COLOR_SCHEME_LIGHT, setAccentColor } from '../src/utils/Colors';
import { MMKV } from '../src/utils/mmkv';
import { ACCENT, COLOR_SCHEME_DARK, COLOR_SCHEME_LIGHT } from '../src/utils/color-scheme';
import { MMKV } from '../src/utils/database/mmkv';
export const useShareStore = create((set, get) => ({
colors: Appearance.getColorScheme() === 'dark' ? COLOR_SCHEME_DARK : COLOR_SCHEME_LIGHT,
accent: ACCENT,
setAccent: async () => {
let accent = await MMKV.getItem('accentColor');
if (accent) {
accent = {
color: accent,
shade: accent + '12'
let appSettings = await MMKV.getItem('appSettings');
if (appSettings) {
appSettings = JSON.parse(appSettings);
let accentColor = appSettings.theme?.accent || ACCENT.color;
let accent = {
color: accentColor,
shade: accentColor + '12'
};
set({ accent: accent });
}

File diff suppressed because one or more lines are too long

View File

@@ -1,62 +0,0 @@
import React from 'react';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { useTracked } from '../../provider';
import { PressableButton } from '../PressableButton';
import { SIZE } from '../../utils/SizeUtils';
import { hexToRGBA, RGB_Linear_Shade } from '../../utils/ColorUtils';
import { showTooltip } from '../../utils';
export const ActionIcon = ({
onPress,
name,
color,
customStyle,
size = SIZE.xxl,
iconStyle = {},
left = 10,
right = 10,
top = 30,
bottom = 10,
testID,
disabled,
onLongPress,
tooltipText,
type = 'gray'
}) => {
const [state, dispatch] = useTracked();
const { colors } = state;
return (
<PressableButton
testID={testID}
onPress={onPress}
hitSlop={{ top: top, left: left, right: right, bottom: bottom }}
onLongPress={event => {
if (onLongPress) {
onLongPress();
return;
}
if (tooltipText) {
showTooltip(event, tooltipText);
}
}}
disabled={disabled}
type={type}
customStyle={{
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 100,
...customStyle
}}
>
<Icon
name={name}
style={iconStyle}
color={disabled ? RGB_Linear_Shade(-0.05, hexToRGBA(colors.nav)) : color}
size={size}
/>
</PressableButton>
);
};

View File

@@ -1,97 +0,0 @@
import React from 'react';
import { View } from 'react-native';
import { useTracked } from '../../provider';
import { eSendEvent, presentSheet } from '../../services/EventManager';
import { eCloseAnnouncementDialog } from '../../utils/Events';
import { openLinkInBrowser } from '../../utils/functions';
import { SIZE } from '../../utils/SizeUtils';
import { sleep } from '../../utils/TimeUtils';
import SettingsBackupAndRestore from '../../views/Settings/backup-restore';
import { Button } from '../Button';
import GeneralSheet from '../GeneralSheet';
import { PricingPlans } from '../Premium/pricing-plans';
import { allowedOnPlatform, getStyle } from './functions';
export const Cta = ({ actions, style = {}, color, inline }) => {
const [state] = useTracked();
const colors = state.colors;
let buttons = actions.filter(item => allowedOnPlatform(item.platforms)) || [];
const onPress = async item => {
if (!inline) {
eSendEvent(eCloseAnnouncementDialog);
await sleep(500);
}
if (item.type === 'link') {
try {
await openLinkInBrowser(item.data, colors);
} catch (e) {}
} else if (item.type === 'promo') {
presentSheet({
component: (
<PricingPlans
marginTop={1}
promo={{
promoCode: item.data,
text: item.title
}}
/>
)
});
} else if (item.type === 'backup') {
presentSheet({
title: 'Backup & restore',
paragraph: 'Please enable automatic backups to keep your data safe',
component: <SettingsBackupAndRestore isSheet={true} />
});
}
};
return (
<View
style={{
paddingHorizontal: 12,
...getStyle(style)
}}
>
<GeneralSheet context="premium_cta" />
{buttons.length > 0 &&
buttons.slice(0, 1).map(item => (
<Button
key={item.title}
title={item.title}
fontSize={SIZE.md}
buttonType={{
color: color ? color : colors.accent,
text: colors.light,
selected: color ? color : colors.accent,
opacity: 1
}}
onPress={() => onPress(item)}
width={'100%'}
style={{
marginBottom: 5
}}
/>
))}
{buttons.length > 1 &&
buttons.slice(1, 2).map((item, index) => (
<Button
key={item.title}
title={item.title}
fontSize={SIZE.xs}
type="gray"
onPress={() => onPress(item)}
width={null}
height={20}
style={{
minWidth: '50%'
}}
textStyle={{
textDecorationLine: 'underline'
}}
/>
))}
</View>
);
};

View File

@@ -1,21 +0,0 @@
import React from 'react';
import { useTracked } from '../../provider';
import { SIZE } from '../../utils/SizeUtils';
import Paragraph from '../Typography/Paragraph';
import { getStyle } from './functions';
export const Description = ({ text, style = {} }) => {
const [state] = useTracked();
const colors = state.colors;
return (
<Paragraph
size={SIZE.md}
style={{
marginHorizontal: 12,
...getStyle(style)
}}
>
{text}
</Paragraph>
);
};

View File

@@ -1,21 +0,0 @@
import React from 'react';
import { useTracked } from '../../provider';
import Heading from '../Typography/Heading';
import { getStyle } from './functions';
export const Title = ({ text, style = {} }) => {
const [state] = useTracked();
const colors = state.colors;
return (
<Heading
style={{
marginHorizontal: 12,
marginTop: 12,
...getStyle(style)
}}
>
{text}
</Heading>
);
};

View File

@@ -1,332 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import { Appearance, NativeModules, Platform, SafeAreaView } from 'react-native';
import { NavigationBar } from 'react-native-bars';
import RNBootSplash from 'react-native-bootsplash';
import { checkVersion } from 'react-native-check-version';
import Animated from 'react-native-reanimated';
import { useTracked } from '../../provider';
import {
useFavoriteStore,
useMessageStore,
useNoteStore,
useSettingStore,
useUserStore
} from '../../provider/stores';
import Backup from '../../services/Backup';
import BiometricService from '../../services/BiometricService';
import { DDS } from '../../services/DeviceDetection';
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
presentSheet,
ToastEvent
} from '../../services/EventManager';
import { setRateAppMessage } from '../../services/Message';
import PremiumService from '../../services/PremiumService';
import { editing } from '../../utils';
import { COLOR_SCHEME_DARK } from '../../utils/Colors';
import { db } from '../../utils/database';
import { eOpenAnnouncementDialog, eOpenLoginDialog } from '../../utils/Events';
import { MMKV } from '../../utils/mmkv';
import { tabBarRef } from '../../utils/Refs';
import { SIZE } from '../../utils/SizeUtils';
import { sleep } from '../../utils/TimeUtils';
import SettingsBackupAndRestore from '../../views/Settings/backup-restore';
import { Button } from '../Button';
import Input from '../Input';
import Seperator from '../Seperator';
import SplashScreen from '../SplashScreen';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
import { Update } from '../Update';
let passwordValue = null;
let didVerifyUser = false;
const AppLoader = ({ onLoad }) => {
const [state] = useTracked();
const colors = state.colors;
const setNotes = useNoteStore(state => state.setNotes);
const setFavorites = useFavoriteStore(state => state.setFavorites);
const _setLoading = useNoteStore(state => state.setLoading);
const _loading = useNoteStore(state => state.loading);
const user = useUserStore(state => state.user);
const verifyUser = useUserStore(state => state.verifyUser);
const setVerifyUser = useUserStore(state => state.setVerifyUser);
const deviceMode = useSettingStore(state => state.deviceMode);
const pwdInput = useRef();
const [requireIntro, setRequireIntro] = useState({
updated: false,
value: false
});
const load = async () => {
if (verifyUser) {
return;
}
await restoreEditorState();
await db.notes.init();
setNotes();
setFavorites();
_setLoading(false);
};
const hideSplashScreen = async () => {
await sleep(requireIntro.value ? 500 : 0);
await RNBootSplash.hide({ fade: true });
setTimeout(async () => {
NativeModules.RNBars.setStatusBarStyle(!colors.night ? 'light-content' : 'dark-content');
await sleep(5);
NativeModules.RNBars.setStatusBarStyle(colors.night ? 'light-content' : 'dark-content');
}, 500);
};
useEffect(() => {
if (requireIntro.updated) {
hideSplashScreen();
}
}, [requireIntro, verifyUser]);
useEffect(() => {
(async () => {
let introCompleted = await MMKV.getItem('introCompleted');
setRequireIntro({
updated: true,
value: !introCompleted
});
})();
if (!_loading) {
(async () => {
await sleep(500);
if ((await MMKV.getItem('loginSessionHasExpired')) === 'expired') {
eSendEvent(eOpenLoginDialog, 4);
return;
}
if (await checkAppUpdateAvailable()) return;
if (await checkForRateAppRequest()) return;
if (await checkNeedsBackup()) return;
if (await PremiumService.getRemainingTrialDaysStatus()) return;
await useMessageStore.getState().setAnnouncement();
if (!requireIntro) {
let dialogs = useMessageStore.getState().dialogs;
if (dialogs.length > 0) {
eSendEvent(eOpenAnnouncementDialog, dialogs[0]);
}
}
})();
}
}, [_loading]);
const checkAppUpdateAvailable = async () => {
try {
const version = await checkVersion();
if (!version.needsUpdate) return false;
presentSheet({
component: ref => <Update version={version} fwdRef={ref} />
});
return true;
} catch (e) {
return false;
}
};
const restoreEditorState = async () => {
let appState = await MMKV.getItem('appState');
if (appState) {
appState = JSON.parse(appState);
if (
appState.note &&
!appState.note.locked &&
!appState.movedAway &&
Date.now() < appState.timestamp + 3600000
) {
editing.isRestoringState = true;
editing.currentlyEditing = true;
editing.movedAway = false;
if (!DDS.isTab) {
tabBarRef.current?.goToPage(1);
}
eSendEvent('loadingNote', appState.note);
}
}
};
const checkForRateAppRequest = async () => {
let askForRating = await MMKV.getItem('askForRating');
if (askForRating !== 'never' || askForRating !== 'completed') {
askForRating = JSON.parse(askForRating);
if (askForRating?.timestamp < Date.now()) {
if (!useMessageStore.getState().message.visible) {
setRateAppMessage();
}
return true;
}
}
return false;
};
const checkNeedsBackup = async () => {
let settingsStore = useSettingStore.getState();
let askForBackup = await MMKV.getItem('askForBackup');
if (settingsStore.settings.reminder === 'off' || !settingsStore.settings.reminder) {
askForBackup = JSON.parse(askForBackup);
if (askForBackup?.timestamp < Date.now()) {
presentSheet({
title: 'Backup & restore',
paragraph: 'Please enable automatic backups to keep your data safe',
component: <SettingsBackupAndRestore isSheet={true} />
});
return true;
}
}
return false;
};
useEffect(() => {
eSubscribeEvent('load_overlay', load);
if (!verifyUser) {
if (!didVerifyUser) {
onLoad();
} else {
load();
}
}
if (verifyUser) {
onUnlockBiometrics();
}
return () => {
eUnSubscribeEvent('load_overlay', load);
};
}, [verifyUser]);
const onUnlockBiometrics = async () => {
if (!(await BiometricService.isBiometryAvailable())) {
ToastEvent.show({
heading: 'Biometrics unavailable',
message: 'Try unlocking the app with your account password'
});
return;
}
let verified = await BiometricService.validateUser('Unlock to access your notes', '');
if (verified) {
didVerifyUser = true;
setVerifyUser(false);
passwordValue = null;
}
};
const onSubmit = async () => {
if (!passwordValue) return;
try {
let verified = await db.user.verifyPassword(passwordValue);
if (verified) {
didVerifyUser = true;
setVerifyUser(false);
passwordValue = null;
}
} catch (e) {}
};
return verifyUser ? (
<Animated.View
style={{
backgroundColor: Appearance.getColorScheme() === 'dark' ? COLOR_SCHEME_DARK.bg : colors.bg,
width: '100%',
height: '100%',
position: 'absolute',
zIndex: 999,
borderRadius: 10
}}
>
<Animated.View
style={{
backgroundColor:
Appearance.getColorScheme() === 'dark' ? COLOR_SCHEME_DARK.bg : colors.bg,
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 10
}}
>
<SafeAreaView
style={{
flex: 1,
justifyContent: 'center',
width: deviceMode !== 'mobile' ? '50%' : Platform.OS == 'ios' ? '95%' : '100%',
paddingHorizontal: 12
}}
>
<Heading
style={{
alignSelf: 'center'
}}
>
Verify your identity
</Heading>
{user ? (
<>
<Paragraph
style={{
alignSelf: 'center'
}}
>
To keep your notes secure, please enter password of the account you are logged in
to.
</Paragraph>
<Seperator />
<Input
fwdRef={pwdInput}
secureTextEntry
placeholder="Enter account password"
onChangeText={v => (passwordValue = v)}
onSubmit={onSubmit}
/>
<Seperator half />
<Button
title="Unlock"
type="accent"
onPress={onSubmit}
width="100%"
height={50}
fontSize={SIZE.md}
/>
<Seperator />
</>
) : (
<>
<Paragraph
style={{
alignSelf: 'center'
}}
>
To keep your notes secure, please unlock app the with biometrics.
</Paragraph>
<Seperator />
</>
)}
<Button
title="Unlock with Biometrics"
width="100%"
height={50}
onPress={onUnlockBiometrics}
icon={'fingerprint'}
type={!user ? 'accent' : 'transparent'}
fontSize={SIZE.md}
/>
</SafeAreaView>
</Animated.View>
</Animated.View>
) : requireIntro.value && !_loading ? (
<SplashScreen />
) : null;
};
export default AppLoader;

View File

@@ -1,284 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import { TouchableOpacity, View } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import * as Progress from 'react-native-progress';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { useTracked } from '../../provider';
import { useAttachmentStore } from '../../provider/stores';
import { eSubscribeEvent, eUnSubscribeEvent } from '../../services/EventManager';
import { db } from '../../utils/database';
import { eCloseAttachmentDialog, eOpenAttachmentsDialog } from '../../utils/Events';
import filesystem from '../../utils/filesystem';
import { SIZE } from '../../utils/SizeUtils';
import { ActionIcon } from '../ActionIcon';
import SheetWrapper from '../Sheet';
import DialogHeader from '../Dialog/dialog-header';
import GeneralSheet from '../GeneralSheet';
import Paragraph from '../Typography/Paragraph';
export const AttachmentDialog = () => {
const [state] = useTracked();
const colors = state.colors;
const [visible, setVisible] = useState(false);
const [note, setNote] = useState(null);
const actionSheetRef = useRef();
const [attachments, setAttachments] = useState([]);
useEffect(() => {
eSubscribeEvent(eOpenAttachmentsDialog, open);
eSubscribeEvent(eCloseAttachmentDialog, close);
return () => {
eUnSubscribeEvent(eOpenAttachmentsDialog, open);
eUnSubscribeEvent(eCloseAttachmentDialog, close);
};
}, [visible]);
const open = item => {
setNote(item);
setVisible(true);
let _attachments = db.attachments.ofNote(item.id, 'all');
setAttachments(_attachments);
};
useEffect(() => {
if (visible) {
actionSheetRef.current?.show();
}
}, [visible]);
const close = () => {
actionSheetRef.current?.hide();
setVisible(false);
};
return !visible ? null : (
<SheetWrapper
centered={false}
fwdRef={actionSheetRef}
onClose={async () => {
setVisible(false);
}}
>
<View
style={{
width: '100%',
alignSelf: 'center',
paddingHorizontal: 12
}}
>
<DialogHeader title="Attachments" />
<FlatList
nestedScrollEnabled
overScrollMode="never"
scrollToOverflowEnabled={false}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
onMomentumScrollEnd={() => {
actionSheetRef.current?.handleChildScrollEnd();
}}
ListEmptyComponent={
<View
style={{
height: 150,
justifyContent: 'center',
alignItems: 'center'
}}
>
<Icon name="attachment" size={60} color={colors.icon} />
<Paragraph>No attachments on this note</Paragraph>
</View>
}
data={attachments}
renderItem={({ item, index }) => (
<Attachment attachment={item} note={note} setNote={setNote} />
)}
/>
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
textAlign: 'center',
marginTop: 10
}}
>
<Icon name="shield-key-outline" size={SIZE.xs} color={colors.icon} />
{' '}All attachments are end-to-end encrypted.
</Paragraph>
</View>
</SheetWrapper>
);
};
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
function getFileExtension(filename) {
var ext = /^.+\.([^.]+)$/.exec(filename);
return ext == null ? '' : ext[1];
}
export const Attachment = ({ attachment, encryption }) => {
const [state] = useTracked();
const colors = state.colors;
const progress = useAttachmentStore(state => state.progress);
const [currentProgress, setCurrentProgress] = useState(
encryption
? {
type: 'encrypt'
}
: null
);
const encryptionProgress = encryption
? useAttachmentStore(state => state.encryptionProgress)
: null;
const onPress = async () => {
if (currentProgress) {
db.fs.cancel(attachment.metadata.hash, 'download');
useAttachmentStore.getState().remove(attachment.metadata.hash);
return;
}
filesystem.downloadAttachment(attachment.metadata.hash, false);
};
useEffect(() => {
let prog = progress[attachment.metadata.hash];
if (prog) {
let type = prog.type;
let loaded = prog.type === 'download' ? prog.recieved : prog.sent;
prog = loaded / prog.total;
prog = (prog * 100).toFixed(0);
console.log('progress: ', prog);
console.log(prog);
setCurrentProgress({
value: prog,
percent: prog + '%',
type: type
});
} else {
setCurrentProgress(null);
}
}, [progress]);
return (
<View
style={{
flexDirection: 'row',
marginVertical: 5,
justifyContent: 'space-between',
padding: 12,
paddingVertical: 6,
borderRadius: 5,
backgroundColor: colors.nav
}}
type="grayBg"
>
<GeneralSheet context={attachment.metadata.hash} />
<View
style={{
flexShrink: 1,
flexDirection: 'row',
alignItems: 'center'
}}
>
<View
style={{
justifyContent: 'center',
alignItems: 'center',
marginLeft: -5
}}
>
<Icon name="file" size={SIZE.xxxl} color={colors.icon} />
<Paragraph
adjustsFontSizeToFit
size={6}
color={colors.light}
style={{
position: 'absolute'
}}
>
{getFileExtension(attachment.metadata.filename).toUpperCase()}
</Paragraph>
</View>
<View
style={{
flexShrink: 1,
marginLeft: 10
}}
>
<Paragraph
size={SIZE.sm - 1}
style={{
flexWrap: 'wrap',
marginBottom: 2.5
}}
numberOfLines={1}
lineBreakMode="middle"
color={colors.pri}
>
{attachment.metadata.filename}
</Paragraph>
<Paragraph color={colors.icon} size={SIZE.xs}>
{formatBytes(attachment.length)}{' '}
{currentProgress?.type ? '(' + currentProgress.type + 'ing - tap to cancel)' : ''}
</Paragraph>
</View>
</View>
{currentProgress || encryptionProgress || encryption ? (
<TouchableOpacity
activeOpacity={0.9}
onPress={() => {
if (encryption) return;
db.fs.cancel(attachment.metadata.hash);
setCurrentProgress(null);
}}
style={{
justifyContent: 'center',
marginLeft: 5,
marginTop: 5,
marginRight: -5
}}
>
<Progress.Circle
size={SIZE.xxl}
progress={
encryptionProgress
? encryptionProgress
: currentProgress?.value
? currentProgress?.value / 100
: 0
}
showsText
textStyle={{
fontSize: 10
}}
color={colors.accent}
formatText={progress => (progress * 100).toFixed(0)}
borderWidth={0}
thickness={2}
/>
</TouchableOpacity>
) : (
<ActionIcon
onPress={() => !encryption && onPress(attachment)}
name="download"
size={SIZE.lg}
color={colors.pri}
/>
)}
</View>
);
};

View File

@@ -1,149 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import { View } from 'react-native';
import { useTracked } from '../../provider';
import { DDS } from '../../services/DeviceDetection';
import { eSubscribeEvent, eUnSubscribeEvent } from '../../services/EventManager';
import { getElevation } from '../../utils';
import { eCloseSimpleDialog, eOpenSimpleDialog } from '../../utils/Events';
import { sleep } from '../../utils/TimeUtils';
import Input from '../Input';
import Seperator from '../Seperator';
import { Toast } from '../Toast';
import BaseDialog from './base-dialog';
import DialogButtons from './dialog-buttons';
import DialogHeader from './dialog-header';
export const Dialog = ({ context = 'global' }) => {
const [state] = useTracked();
const colors = state.colors;
const [visible, setVisible] = useState(false);
const [inputValue, setInputValue] = useState(null);
const inputRef = useRef();
const [dialogInfo, setDialogInfo] = useState({
title: '',
paragraph: '',
positiveText: 'Done',
negativeText: 'Cancel',
positivePress: () => {},
onClose: () => {},
positiveType: 'transparent',
icon: null,
paragraphColor: colors.pri,
input: false,
inputPlaceholder: 'Enter some text',
defaultValue: '',
disableBackdropClosing: false
});
useEffect(() => {
eSubscribeEvent(eOpenSimpleDialog, show);
eSubscribeEvent(eCloseSimpleDialog, hide);
return () => {
eUnSubscribeEvent(eOpenSimpleDialog, show);
eUnSubscribeEvent(eCloseSimpleDialog, hide);
};
}, []);
const onPressPositive = async () => {
if (dialogInfo.positivePress) {
inputRef.current?.blur();
let result = await dialogInfo.positivePress(inputValue || dialogInfo.defaultValue);
if (result === false) {
return;
}
}
hide();
};
const show = data => {
if (!data.context) data.context = 'global';
if (data.context !== context) return;
setDialogInfo(data);
setVisible(true);
};
const hide = () => {
setVisible(false);
};
const onNegativePress = async () => {
if (dialogInfo.onClose) {
await dialogInfo.onClose();
}
hide();
};
const style = {
...getElevation(5),
width: DDS.isTab ? 400 : '85%',
maxHeight: 450,
borderRadius: 5,
backgroundColor: colors.bg,
paddingTop: 12
};
return (
visible && (
<BaseDialog
statusBarTranslucent={false}
bounce={!dialogInfo.input}
closeOnTouch={!dialogInfo.disableBackdropClosing}
onShow={async () => {
if (dialogInfo.input) {
inputRef.current?.setNativeProps({
text: dialogInfo.defaultValue
});
await sleep(300);
inputRef.current?.focus();
}
}}
visible={true}
onRequestClose={hide}
>
<View style={style}>
<DialogHeader
title={dialogInfo.title}
icon={dialogInfo.icon}
paragraph={dialogInfo.paragraph}
paragraphColor={dialogInfo.paragraphColor}
padding={12}
/>
<Seperator half />
{dialogInfo.input ? (
<View
style={{
paddingHorizontal: 12
}}
>
<Input
fwdRef={inputRef}
autoCapitalize="none"
onChangeText={value => {
setInputValue(value);
}}
secureTextEntry={dialogInfo.secureTextEntry}
//defaultValue={dialogInfo.defaultValue}
onSubmit={onPressPositive}
returnKeyLabel="Done"
returnKeyType="done"
placeholder={dialogInfo.inputPlaceholder}
/>
</View>
) : null}
<DialogButtons
onPressNegative={onNegativePress}
onPressPositive={dialogInfo.positivePress && onPressPositive}
positiveTitle={dialogInfo.positiveText}
negativeTitle={dialogInfo.negativeText}
positiveType={dialogInfo.positiveType}
/>
</View>
<Toast context="local" />
</BaseDialog>
)
);
};

View File

@@ -1,81 +0,0 @@
import React, { Component } from 'react';
import { eSubscribeEvent, eUnSubscribeEvent } from '../../services/EventManager';
import { getCurrentColors } from '../../utils/Colors';
import { eThemeUpdated } from '../../utils/Events';
import { EditorSettings } from '../../views/Editor/EditorSettings';
import { AddNotebookDialog } from '../AddNotebookDialog';
import { AddTopicDialog } from '../AddTopicDialog';
import { AnnouncementDialog } from '../Announcements';
import { AttachmentDialog } from '../AttachmentDialog';
import { Dialog } from '../Dialog';
import ExportDialog from '../ExportDialog';
import GeneralSheet from '../GeneralSheet';
import ImagePreview from '../ImagePreview';
import LoginDialog from '../LoginDialog';
import MergeEditor from '../MergeEditor';
import MoveNoteDialog from '../MoveNoteDialog';
import PremiumDialog from '../Premium';
import { Expiring } from '../Premium/expiring';
import PublishNoteDialog from '../PublishNoteDialog';
import RateDialog from '../RateDialog';
import RecoveryKeyDialog from '../RecoveryKeyDialog';
import RestoreDialog from '../RestoreDialog';
import ResultDialog from '../ResultDialog';
import TagsDialog from '../TagsDialog';
import { VaultDialog } from '../VaultDialog';
export class DialogManager extends Component {
constructor(props) {
super(props);
this.state = {
colors: getCurrentColors()
};
}
shouldComponentUpdate(nextProps, nextState) {
return JSON.stringify(nextProps) !== JSON.stringify(this.props) || nextState !== this.state;
}
onThemeChange = () => {
this.setState({
colors: getCurrentColors()
});
};
componentDidMount() {
eSubscribeEvent(eThemeUpdated, this.onThemeChange);
}
componentWillUnmount() {
eUnSubscribeEvent(eThemeUpdated, this.onThemeChange);
}
render() {
let { colors } = this.state;
return (
<>
<Dialog context="global" />
<AddTopicDialog colors={colors} />
<AddNotebookDialog colors={colors} />
<PremiumDialog colors={colors} />
<LoginDialog colors={colors} />
<MergeEditor />
<ExportDialog />
<RecoveryKeyDialog colors={colors} />
<GeneralSheet />
<RestoreDialog />
<ResultDialog />
<VaultDialog colors={colors} />
<MoveNoteDialog colors={colors} />
<RateDialog />
<ImagePreview />
<EditorSettings />
<PublishNoteDialog />
<TagsDialog />
<AttachmentDialog />
<Expiring />
<AnnouncementDialog />
</>
);
}
}

View File

@@ -1,69 +0,0 @@
import React, { useState } from 'react';
import { View } from 'react-native';
import { SvgXml } from 'react-native-svg';
import {
FAV_SVG,
LOGIN_SVG,
LOGO_SVG,
NOTEBOOK_SVG,
NOTE_SVG,
SEARCH_SVG,
SETTINGS_SVG,
TAG_SVG,
TOPIC_SVG,
TRASH_SVG
} from '../../assets/images/assets';
import { useTracked } from '../../provider';
export const Placeholder = ({ type, w, h, color }) => {
const [state, dispatch] = useTracked();
const { colors } = state;
const getSVG = () => {
switch (type) {
case 'notes':
return NOTE_SVG(color || colors.accent);
case 'notebooks':
return NOTEBOOK_SVG(colors.accent);
case 'topics':
return TOPIC_SVG(colors.accent);
case 'tags':
return TAG_SVG(colors.accent);
case 'favorites':
return FAV_SVG(colors.accent);
case 'trash':
return TRASH_SVG(colors.accent);
case 'settings':
return SETTINGS_SVG(colors.accent);
case 'search':
return SEARCH_SVG(colors.accent);
case 'login':
return LOGIN_SVG(colors.accent);
case 'signup':
return LOGO_SVG;
}
};
return (
<SvgToPngView
color={type === 'notes' ? color || colors.accent : colors.accent}
src={getSVG()}
img={type}
width={w}
height={h}
/>
);
};
export const SvgToPngView = ({ width, height, src, color, img }) => {
const [error, setError] = useState(false);
return (
<View
style={{
height: width || 250,
width: height || 250
}}
>
<SvgXml xml={src} width="100%" height="100%" />
</View>
);
};

View File

@@ -1,80 +0,0 @@
import React from 'react';
import { ActivityIndicator, StyleSheet, View } from 'react-native';
import { useTracked } from '../../provider';
import { ph, pv, SIZE } from '../../utils/SizeUtils';
import { Button } from '../Button';
import Paragraph from '../Typography/Paragraph';
export const Loading = ({
height = 150,
tagline = 'Loading....',
done = false,
doneText = 'Action completed successfully!',
onDone = () => {},
customStyle = {}
}) => {
const [state, dispatch] = useTracked();
const { colors } = state;
return (
<View
style={[
{ height: height, backgroundColor: colors.bg },
styles.activityContainer,
customStyle
]}
>
{done ? (
<>
<Paragraph color={colors.icon} size={SIZE.xs} style={styles.activityText}>
{doneText}
</Paragraph>
<Button onPress={onDone} title="Open file" />
</>
) : (
<>
<ActivityIndicator color={colors.accent} />
<Paragraph
size={SIZE.md}
style={{
marginTop: 10
}}
color={colors.pri}
>
{tagline}
</Paragraph>
</>
)}
</View>
);
};
const styles = StyleSheet.create({
activityText: {
fontSize: SIZE.sm,
textAlign: 'center',
marginBottom: 10
},
activityContainer: {
alignItems: 'center',
justifyContent: 'center'
},
button: {
paddingVertical: pv,
paddingHorizontal: ph,
marginTop: 10,
borderRadius: 5,
alignSelf: 'center',
width: '48%',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
flexDirection: 'row'
},
buttonText: {
//fontFamily: "sans-serif",
color: 'white',
fontSize: SIZE.sm
}
});

View File

@@ -1,806 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { Button } from '../../components/Button';
import Seperator from '../../components/Seperator';
import { useTracked } from '../../provider/index';
import { useUserStore } from '../../provider/stores';
import BiometricService from '../../services/BiometricService';
import { DDS } from '../../services/DeviceDetection';
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
presentSheet,
ToastEvent
} from '../../services/EventManager';
import { clearMessage, setEmailVerifyMessage } from '../../services/Message';
import PremiumService from '../../services/PremiumService';
import Sync from '../../services/Sync';
import { hexToRGBA } from '../../utils/ColorUtils';
import { db } from '../../utils/database';
import { eOpenLoginDialog, eOpenResultDialog } from '../../utils/Events';
import { openLinkInBrowser } from '../../utils/functions';
import { MMKV } from '../../utils/mmkv';
import { SIZE } from '../../utils/SizeUtils';
import Storage from '../../utils/storage';
import { sleep } from '../../utils/TimeUtils';
import SheetWrapper from '../Sheet';
import BaseDialog from '../Dialog/base-dialog';
import DialogButtons from '../Dialog/dialog-buttons';
import DialogContainer from '../Dialog/dialog-container';
import DialogHeader from '../Dialog/dialog-header';
import Input from '../Input';
import { Header } from '../SimpleList/header';
import Paragraph from '../Typography/Paragraph';
import { ActionIcon } from '../ActionIcon';
import umami from '../../utils/umami';
const MODES = {
login: 0,
signup: 1,
forgotPassword: 2,
changePassword: 3,
sessionExpired: 4
};
let email = '';
let password = '';
let confirmPassword;
let oldPassword;
function getEmail() {
if (!email) return null;
return email.replace(/(.{2})(.*)(?=@)/, function (gp1, gp2, gp3) {
for (let i = 0; i < gp3.length; i++) {
gp2 += '*';
}
return gp2;
});
}
const LoginDialog = () => {
const [state, dispatch] = useTracked();
const colors = state.colors;
const setUser = useUserStore(state => state.setUser);
const setLastSynced = useUserStore(state => state.setLastSynced);
const [visible, setVisible] = useState(false);
const [status, setStatus] = useState(null);
const [loading, setLoading] = useState(false);
const [userConsent, setUserConsent] = useState(true);
const [mode, setMode] = useState(MODES.login);
const [error, setError] = useState(false);
const [focused, setFocused] = useState(false);
const [confirm, setConfirm] = useState(false);
const scrollViewRef = useRef();
const actionSheetRef = useRef();
const _email = useRef();
const _pass = useRef();
const _oPass = useRef();
const _passConfirm = useRef();
const MODE_DATA = [
{
headerButton: 'Login',
headerButtonFunc: () => {
setMode(MODES.signup);
},
button: 'Login',
buttonFunc: () => loginUser(),
headerParagraph: 'Create a new account',
showForgotButton: true,
loading: 'Please wait while we log in and sync your data.',
showLoader: true,
buttonAlt: 'Sign Up',
buttonAltFunc: () => {
setMode(MODES.signup);
}
},
{
headerButton: 'Sign Up',
headerButtonFunc: () => {
_email.current?.blur();
setMode(MODES.login);
onChangeFocus();
},
button: 'Create Account',
buttonFunc: () => signupUser(),
headerParagraph: 'Login to your account',
showForgotButton: false,
loading: 'Please wait while we set up your account.',
showLoader: true,
buttonAlt: 'Login',
buttonAltFunc: () => {
_email.current?.blur();
setMode(MODES.login);
onChangeFocus();
}
},
{
headerButton: 'Forgot Password',
headerButtonFunc: () => {
_email.current?.blur();
setMode(MODES.login);
onChangeFocus();
},
button: 'Send Recovery Email',
buttonFunc: () => sendEmail(),
headerParagraph: 'Login to your account',
showForgotButton: false,
loading:
'Please follow the link in the email to set up your new password. If you are unable to find our email, check your spam folder.',
showLoader: false,
buttonAlt: 'Go back to Login',
buttonAltFunc: () => {
setMode(MODES.login);
}
},
{
headerButton: 'Change Password',
headerButtonFunc: () => {
setMode(MODES.signup);
},
button: 'Change Password',
buttonFunc: () => changePassword(),
headerParagraph: 'login to your account',
showForgotButton: false,
loading: 'Please wait while we change your password and encrypt your data.',
showLoader: true,
buttonAlt: null
},
{
headerButton: 'Session expired',
headerButtonFunc: () => {},
button: 'Login',
buttonFunc: () => loginUser(),
headerParagraph: '',
showForgotButton: true,
loading: 'Please wait while we log in and sync your data.',
showLoader: true,
buttonAlt: 'Logout',
buttonAltFunc: () => {
setConfirm(true);
}
}
];
const current = MODE_DATA[mode];
useEffect(() => {
eSubscribeEvent(eOpenLoginDialog, open);
return () => {
eUnSubscribeEvent(eOpenLoginDialog, open);
};
}, []);
async function open(mode) {
setMode(mode ? mode : MODES.login);
if (mode === MODES.sessionExpired) {
try {
console.log('REQUESTING NEW TOKEN');
let res = await db.user.tokenManager.getToken();
if (!res) throw new Error('no token found');
if (db.user.tokenManager._isTokenExpired(res)) throw new Error('token expired');
if (!(await Sync.run())) throw new Error('e');
await MMKV.removeItem('loginSessionHasExpired');
return;
} catch (e) {
console.log(e);
let user = await db.user.getUser();
if (!user) return;
email = user.email;
}
}
setStatus(null);
setVisible(true);
await sleep(10);
actionSheetRef.current?.show();
}
const close = () => {
if (loading) return;
actionSheetRef.current?.hide();
confirmPassword = null;
oldPassword = null;
email = null;
password = null;
setError(false);
setLoading(false);
setStatus(null);
setMode(MODES.login);
setVisible(false);
};
const loginUser = async () => {
if (!password || !email || error) {
ToastEvent.show({
heading: 'Email or password is invalid',
type: 'error',
context: 'local'
});
return;
}
setLoading(true);
_email.current?.blur();
_pass.current?.blur();
setStatus('Logging in');
let user;
try {
await db.user.login(email.toLowerCase(), password);
user = await db.user.getUser();
if (!user) throw new Error('Email or password incorrect!');
setStatus('Syncing Your Data');
PremiumService.setPremiumStatus();
setUser(user);
clearMessage();
ToastEvent.show({
heading: 'Login successful',
message: `Logged in as ${user.email}`,
type: 'success',
context: 'local'
});
close();
await MMKV.removeItem('loginSessionHasExpired');
eSendEvent('userLoggedIn', true);
console.log('PRESENTING SHEET');
await sleep(500);
presentSheet({
title: 'Syncing your data',
paragraph: 'Please wait while we sync all your data.',
progress: true
});
} catch (e) {
setLoading(false);
setStatus(null);
ToastEvent.show({
heading: user ? 'Failed to sync' : 'Login failed',
message: e.message,
type: 'error',
context: 'local'
});
}
};
const validateInfo = () => {
if (!password || !email || !confirmPassword) {
ToastEvent.show({
heading: 'All fields required',
message: 'Fill all the fields and try again',
type: 'error',
context: 'local'
});
return false;
}
if (error) {
ToastEvent.show({
heading: 'Invalid signup information',
message: 'Some or all information provided is invalid. Resolve all errors and try again.',
type: 'error',
context: 'local'
});
return false;
}
if (!userConsent) {
ToastEvent.show({
heading: 'Cannot signup',
message: 'You must agree to our terms of service and privacy policy.',
type: 'error',
context: 'local',
actionText: 'I Agree',
duration: 5000,
func: () => {
setUserConsent(true);
signupUser();
ToastEvent.hide();
}
});
return false;
}
return true;
};
const signupUser = async () => {
if (!validateInfo()) return;
setLoading(true);
setStatus('Creating Account');
try {
await db.user.signup(email.toLowerCase(), password);
let user = await db.user.getUser();
setStatus('Setting Crenditials');
setUser(user);
setLastSynced(await db.lastSynced());
clearMessage();
setEmailVerifyMessage();
close();
umami.pageView('/account-created', '/welcome/signup');
await sleep(300);
eSendEvent(eOpenResultDialog);
} catch (e) {
setStatus(null);
setLoading(false);
ToastEvent.show({
heading: 'Signup failed',
message: e.message,
type: 'error',
context: 'local'
});
}
};
const sendEmail = async nostatus => {
if (!email || error) {
ToastEvent.show({
heading: 'Account email is required.',
type: 'error',
context: 'local'
});
return;
}
try {
let lastRecoveryEmailTime = await MMKV.getItem('lastRecoveryEmailTime');
if (lastRecoveryEmailTime && Date.now() - JSON.parse(lastRecoveryEmailTime) < 60000 * 3) {
throw new Error('Please wait before requesting another email');
}
!nostatus && setStatus('Password Recovery Email Sent!');
await db.user.recoverAccount(email.toLowerCase());
await MMKV.setItem('lastRecoveryEmailTime', JSON.stringify(Date.now()));
ToastEvent.show({
heading: `Check your email to reset password`,
message: `Recovery email has been sent to ${email.toLowerCase()}`,
type: 'success',
context: 'local',
duration: 7000
});
} catch (e) {
setStatus(null);
ToastEvent.show({
heading: 'Recovery email not sent',
message: e.message,
type: 'error',
context: 'local'
});
}
};
const changePassword = async () => {
if (error || !oldPassword || !password) {
ToastEvent.show({
heading: 'All fields required',
message: 'Fill all the fields and try again.',
type: 'error',
context: 'local'
});
return;
}
setLoading(true);
setStatus('Setting new Password');
try {
await db.user.changePassword(oldPassword, password);
ToastEvent.show({
heading: `Account password updated`,
type: 'success',
context: 'local'
});
} catch (e) {
setStatus(null);
ToastEvent.show({
heading: 'Failed to change password',
message: e.message,
type: 'error',
context: 'local'
});
}
setStatus(null);
setLoading(false);
};
const onChangeFocus = () => {
setFocused(!focused);
setTimeout(() => {
_email.current?.focus();
}, 50);
};
return !visible ? null : (
<SheetWrapper
fwdRef={actionSheetRef}
animation={DDS.isTab ? 'fade' : 'slide'}
statusBarTranslucent={false}
gestureEnabled={MODES.sessionExpired !== mode}
closeOnTouchBackdrop={MODES.sessionExpired !== mode}
onRequestClose={MODES.sessionExpired !== mode && close}
keyboardMode="position"
visible={true}
onClose={close}
onOpen={() => {
setTimeout(() => {
if (MODES.sessionExpired === mode) {
_pass.current?.focus();
setFocused(true);
return;
}
_email.current?.focus();
setFocused(true);
}, 500);
}}
background={!DDS.isTab ? colors.bg : null}
transparent={true}
>
{confirm && (
<BaseDialog
onRequestClose={() => {
setConfirm(false);
}}
visible
>
<DialogContainer>
<DialogHeader
title="Logout"
paragraph="All user data on this device will be cleared including any unsynced changes. Do you want to proceed?"
paragraphColor="red"
padding={12}
/>
<Seperator />
<DialogButtons
negativeTitle="Cancel"
onPressNegative={() => {
setConfirm(false);
}}
positiveType="error"
positiveTitle="Logout"
onPressPositive={async () => {
try {
await db.user.logout();
await BiometricService.resetCredentials();
await Storage.write('introCompleted', 'true');
setConfirm(false);
close();
} catch (e) {
ToastEvent.show({
heading: e.message,
type: 'error',
context: 'local'
});
}
}}
/>
</DialogContainer>
</BaseDialog>
)}
{status ? (
<BaseDialog
visible={true}
transparent={current.showLoader}
animation="fade"
onRequestClose={() => {
if (!current.showLoader) {
setStatus(null);
}
}}
/>
) : null}
<ScrollView
ref={scrollViewRef}
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
nestedScrollEnabled={mode !== MODES.sessionExpired}
onMomentumScrollEnd={() => {
actionSheetRef.current.handleChildScrollEnd();
}}
bounces={false}
overScrollMode="never"
scrollToOverflowEnabled="false"
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.bg,
zIndex: 10,
minHeight: DDS.isTab ? '50%' : '85%'
}}
>
<Header
color="transparent"
type="login"
height={150}
noAnnouncement={true}
shouldShow
title={current.headerButton}
messageCard={false}
/>
{mode === MODES.sessionExpired && (
<View
style={{
width: '100%',
paddingHorizontal: 12,
paddingVertical: 12,
marginTop: 10,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: hexToRGBA(colors.red, 0.2)
}}
>
<Icon
size={20}
style={{ marginRight: 10 }}
name="information"
color={colors.errorText}
/>
<Paragraph style={{ maxWidth: '90%' }} color={colors.errorText}>
Please log in to your account to access your notes on this device and sync them.
</Paragraph>
</View>
)}
{mode === MODES.signup && (
<View
style={{
width: '100%',
backgroundColor: colors.shade,
paddingHorizontal: 12,
paddingVertical: 12,
marginTop: 10,
flexDirection: 'row',
alignItems: 'center'
}}
>
<Icon size={20} style={{ marginRight: 10 }} name="information" color={colors.accent} />
<Paragraph size={SIZE.xs + 1} style={{ maxWidth: '90%' }} color={colors.accent}>
Your 14 day free trial of Notesnook Pro will activate when you sign up.{' '}
<Paragraph size={SIZE.xs + 1} color={colors.accent} style={{ fontWeight: 'bold' }}>
No credit card is required.
</Paragraph>
</Paragraph>
</View>
)}
<View
style={{
paddingHorizontal: 12,
paddingTop: 12,
width: focused ? '100%' : '99.9%'
}}
>
{mode === MODES.changePassword ? null : (
<Input
fwdRef={_email}
onChangeText={value => {
email = value;
}}
onErrorCheck={r => {
setError(r);
}}
loading={MODES.sessionExpired === mode}
defaultValue={MODES.sessionExpired === mode ? getEmail() : null}
onFocusInput={onChangeFocus}
returnKeyLabel="Next"
returnKeyType="next"
autoCompleteType="email"
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage="Email is invalid"
placeholder="Email"
onSubmit={() => {
if (mode === MODES.signup || mode === MODES.login) {
_pass.current?.focus();
}
}}
/>
)}
{mode !== MODES.changePassword ? null : (
<Input
fwdRef={_oPass}
onChangeText={value => {
oldPassword = value;
}}
onErrorCheck={r => {
setError(r);
}}
returnKeyLabel="Next"
returnKeyType="next"
secureTextEntry
autoCompleteType="password"
autoCapitalize="none"
autoCorrect={false}
placeholder="Current password"
onSubmit={() => {
if (mode === MODES.signup) {
_pass.current?.focus();
}
}}
/>
)}
{mode === MODES.forgotPassword ? null : (
<>
<Input
fwdRef={_pass}
onChangeText={value => {
password = value;
}}
onErrorCheck={r => {
setError(r);
}}
marginBottom={0}
validationType={mode === MODES.signup ? 'password' : null}
secureTextEntry
autoCompleteType="password"
autoCapitalize="none"
autoCorrect={false}
placeholder="Password"
returnKeyLabel={mode === MODES.signup ? 'Next' : 'Login'}
returnKeyType={mode === MODES.signup ? 'next' : 'done'}
errorMessage={mode === MODES.signup && 'Password is invalid'}
onSubmit={() => {
if (mode === MODES.signup || mode === MODES.changePassword) {
_passConfirm.current?.focus();
} else {
current.buttonFunc();
}
}}
/>
</>
)}
{mode === MODES.login || mode === MODES.sessionExpired ? (
<Button
title="Forgot password?"
style={{
alignSelf: 'flex-end',
height: 30
}}
onPress={() => {
if (MODES.sessionExpired === mode) {
sendEmail(true);
return;
}
setMode(MODES.forgotPassword);
}}
/>
) : null}
<Seperator />
{mode !== MODES.signup && mode !== MODES.changePassword ? null : (
<>
<Input
fwdRef={_passConfirm}
onChangeText={value => {
confirmPassword = value;
}}
onErrorCheck={r => {
setError(r);
}}
loading={loading}
validationType="confirmPassword"
autoCapitalize="none"
returnKeyLabel="Done"
returnKeyType="done"
autoCorrect={false}
customValidator={() => password}
secureTextEntry
placeholder="Confirm password"
errorMessage="Passwords do not match"
onSubmit={() => {
current.buttonFunc();
}}
/>
</>
)}
{mode !== MODES.signup ? null : (
<>
<TouchableOpacity
disabled={loading}
activeOpacity={0.7}
style={{
flexDirection: 'row',
width: '100%',
alignItems: 'center'
}}
>
<Paragraph
size={11}
style={{
maxWidth: '90%'
}}
>
By signing up you agree to our{' '}
<Paragraph
size={11}
onPress={() => {
openLinkInBrowser('https://notesnook.com/tos', colors)
.catch(e => {})
.then(r => {});
}}
color={colors.accent}
>
terms of service{' '}
</Paragraph>
and{' '}
<Paragraph
size={11}
onPress={() => {
openLinkInBrowser('https://notesnook.com/privacy', colors)
.catch(e => {})
.then(r => {});
}}
color={colors.accent}
>
privacy policy.
</Paragraph>
</Paragraph>
</TouchableOpacity>
</>
)}
{mode !== MODES.signup ? null : <Seperator />}
<Button
title={loading ? '' : current.button}
onPress={current.buttonFunc}
width="100%"
type="accent"
fontSize={SIZE.md}
loading={loading}
height={50}
/>
{current.buttonAlt ? (
<Button
title={current.buttonAlt}
onPress={current.buttonAltFunc}
width="100%"
type={MODES.sessionExpired === mode ? 'error' : 'shade'}
fontSize={SIZE.md}
style={{
marginTop: 10
}}
height={50}
/>
) : null}
{loading && mode === MODES.changePassword ? (
<View
style={{
backgroundColor: colors.warningBg,
width: '100%',
borderRadius: 10,
marginTop: 10,
padding: 12,
flexDirection: 'row',
alignItems: 'center',
flexShrink: 1
}}
>
<ActionIcon name="alert" color={colors.warningText} />
<Paragraph
style={{
flexShrink: 1,
marginLeft: 5
}}
color={colors.warningText}
>
Do not close the app or move it to background while we change your password.
</Paragraph>
</View>
) : null}
</View>
<View
style={{
height: 100
}}
/>
</ScrollView>
</SheetWrapper>
);
};
export default LoginDialog;

View File

@@ -1,142 +0,0 @@
import React, { useEffect, useState } from 'react';
import { View } from 'react-native';
import { useTracked } from '../../provider';
import { useMenuStore, useNoteStore } from '../../provider/stores';
import { eSendEvent, eSubscribeEvent, eUnSubscribeEvent } from '../../services/EventManager';
import Navigation from '../../services/Navigation';
import { COLORS_NOTE } from '../../utils/Colors';
import { db } from '../../utils/database';
import { refreshNotesPage } from '../../utils/Events';
import { normalize, SIZE } from '../../utils/SizeUtils';
import { presentDialog } from '../Dialog/functions';
import { PressableButton } from '../PressableButton';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
export const ColorSection = () => {
const colorNotes = useMenuStore(state => state.colorNotes);
const loading = useNoteStore(state => state.loading);
const setColorNotes = useMenuStore(state => state.setColorNotes);
useEffect(() => {
if (!loading) {
setColorNotes();
}
}, [loading]);
return colorNotes.map((item, index) => <ColorItem key={item.id} item={item} index={index} />);
};
const ColorItem = ({ item, index }) => {
const [state] = useTracked();
const { colors } = state;
const setColorNotes = useMenuStore(state => state.setColorNotes);
const [headerTextState, setHeaderTextState] = useState(null);
const alias = db.colors.alias(item.id);
const onHeaderStateChange = event => {
if (event?.id === item.id) {
setHeaderTextState(event);
} else {
setHeaderTextState(null);
}
};
useEffect(() => {
eSubscribeEvent('onHeaderStateChange', onHeaderStateChange);
return () => {
eUnSubscribeEvent('onHeaderStateChange', onHeaderStateChange);
};
}, []);
const onPress = item => {
let params = {
...item,
type: 'color',
menu: true,
get: 'colored'
};
eSendEvent(refreshNotesPage, params);
Navigation.navigate('NotesPage', params, {
heading: alias.slice(0, 1).toUpperCase() + alias.slice(1),
id: item.id,
type: 'color'
});
Navigation.closeDrawer();
};
const onLongPress = () => {
presentDialog({
title: 'Rename color',
input: true,
inputPlaceholder: 'Enter name for this color',
defaultValue: alias,
paragraph: 'You are renaming the color ' + item.title,
positivePress: async value => {
if (!value || value.trim().length === 0) return;
await db.colors.rename(item.id, value);
setColorNotes();
console.log('color updated');
},
positiveText: 'Rename'
});
};
return (
<PressableButton
customColor={headerTextState?.id === item.id ? 'rgba(0,0,0,0.04)' : 'transparent'}
onLongPress={onLongPress}
customSelectedColor={COLORS_NOTE[item.title.toLowerCase()]}
customAlpha={!colors.night ? -0.02 : 0.02}
customOpacity={0.12}
onPress={() => onPress(item)}
customStyle={{
width: '100%',
alignSelf: 'center',
borderRadius: 5,
flexDirection: 'row',
paddingHorizontal: 8,
justifyContent: 'space-between',
alignItems: 'center',
height: normalize(50),
marginBottom: 5
}}
>
<View
style={{
flexDirection: 'row',
alignItems: 'center'
}}
>
<View
style={{
width: 30,
justifyContent: 'center',
alignItems: 'flex-start'
}}
>
<View
style={{
width: SIZE.lg - 2,
height: SIZE.lg - 2,
backgroundColor: COLORS_NOTE[item.title.toLowerCase()],
borderRadius: 100,
justifyContent: 'center',
marginRight: 10
}}
/>
</View>
{headerTextState?.id === item.id ? (
<Heading color={colors.heading} size={SIZE.md}>
{alias.slice(0, 1).toUpperCase() + alias.slice(1)}
</Heading>
) : (
<Paragraph color={colors.pri} size={SIZE.md}>
{alias.slice(0, 1).toUpperCase() + alias.slice(1)}
</Paragraph>
)}
</View>
</PressableButton>
);
};

View File

@@ -1,125 +0,0 @@
import React, { useEffect, useState } from 'react';
import { View } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { useTracked } from '../../provider';
import { eSubscribeEvent, eUnSubscribeEvent } from '../../services/EventManager';
import Navigation from '../../services/Navigation';
import { getElevation } from '../../utils';
import { normalize, SIZE } from '../../utils/SizeUtils';
import { Button } from '../Button';
import { PressableButton } from '../PressableButton';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
import ToggleSwitch from 'toggle-switch-react-native';
export const MenuListItem = ({ item, index, noTextMode, testID, rightBtn }) => {
const [state, dispatch] = useTracked();
const { colors } = state;
const [headerTextState, setHeaderTextState] = useState(null);
let isFocused = headerTextState?.id === item.name.toLowerCase() + '_navigation';
const _onPress = event => {
if (item.func) {
item.func();
} else {
Navigation.navigate(
item.name,
{
menu: true
},
{
heading: item.name,
id: item.name.toLowerCase() + '_navigation'
}
);
}
if (item.close) {
Navigation.closeDrawer();
}
};
const onHeaderStateChange = event => {
if (event.id === item.name.toLowerCase() + '_navigation') {
setHeaderTextState(event);
} else {
setHeaderTextState(null);
}
};
useEffect(() => {
eSubscribeEvent('onHeaderStateChange', onHeaderStateChange);
return () => {
eUnSubscribeEvent('onHeaderStateChange', onHeaderStateChange);
};
}, []);
return (
<PressableButton
testID={testID}
key={item.name + index}
onPress={_onPress}
type={!isFocused ? 'gray' : 'grayBg'}
customStyle={{
width: '100%',
alignSelf: 'center',
borderRadius: 5,
flexDirection: 'row',
paddingHorizontal: 8,
justifyContent: 'space-between',
alignItems: 'center',
height: normalize(50),
marginBottom: 5
}}
>
<View
style={{
flexDirection: 'row',
alignItems: 'center'
}}
>
<Icon
style={{
width: 30,
textAlignVertical: 'center',
textAlign: 'left'
}}
name={item.icon}
color={item.icon === 'crown' ? colors.yellow : isFocused ? colors.accent : colors.pri}
size={SIZE.lg - 2}
/>
{isFocused ? (
<Heading color={colors.heading} size={SIZE.md}>
{item.name}
</Heading>
) : (
<Paragraph size={SIZE.md}>{item.name}</Paragraph>
)}
</View>
{item.switch ? (
<ToggleSwitch
isOn={item.on}
onColor={colors.accent}
offColor={colors.icon}
size="small"
animationSpeed={150}
onToggle={_onPress}
/>
) : rightBtn ? (
<Button
title={rightBtn.name}
type="shade"
height={30}
fontSize={SIZE.xs}
iconSize={SIZE.xs}
icon={rightBtn.icon}
style={{
borderRadius: 100,
paddingHorizontal: 16
}}
onPress={rightBtn.func}
/>
) : null}
</PressableButton>
);
};

View File

@@ -1,222 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import { FlatList, View } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { useTracked } from '../../provider';
import { useMenuStore, useNoteStore } from '../../provider/stores';
import { eSendEvent, eSubscribeEvent, eUnSubscribeEvent } from '../../services/EventManager';
import Navigation from '../../services/Navigation';
import { db } from '../../utils/database';
import { eOnNewTopicAdded, refreshNotesPage } from '../../utils/Events';
import { normalize, SIZE } from '../../utils/SizeUtils';
import SheetWrapper from '../Sheet';
import { Button } from '../Button';
import { ActionSheetEvent } from '../DialogManager/recievers';
import { PressableButton } from '../PressableButton';
import Seperator from '../Seperator';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
import { Properties } from '../Properties';
export const TagsSection = () => {
const menuPins = useMenuStore(state => state.menuPins);
const loading = useNoteStore(state => state.loading);
const setMenuPins = useMenuStore(state => state.setMenuPins);
useEffect(() => {
if (!loading) {
setMenuPins();
}
}, [loading]);
const onPress = item => {
let params = {};
if (item.type === 'notebook') {
params = {
notebook: item,
title: item.title,
menu: true
};
eSendEvent(eOnNewTopicAdded, params);
Navigation.navigate('Notebook', params, {
heading: item.title,
id: item.id,
type: item.type
});
} else if (item.type === 'tag') {
params = {
...item,
type: 'tag',
menu: true,
get: 'tagged'
};
eSendEvent(refreshNotesPage, params);
Navigation.navigate('NotesPage', params, {
heading: '#' + db.tags.alias(item.id),
id: item.id,
type: item.type
});
} else {
params = { ...item, menu: true, get: 'topics' };
eSendEvent(refreshNotesPage, params);
Navigation.navigate('NotesPage', params, {
heading: item.title,
id: item.id,
type: item.type
});
}
Navigation.closeDrawer();
};
return (
<View
style={{
flexGrow: 1
}}
>
<FlatList
data={menuPins}
style={{
flexGrow: 1
}}
contentContainerStyle={{
flexGrow: 1
}}
keyExtractor={(item, index) => item.id}
renderItem={({ item, index }) => <PinItem item={item} index={index} onPress={onPress} />}
/>
</View>
);
};
const PinItem = ({ item, index, onPress }) => {
const [state, dispatch] = useTracked();
const { colors } = state;
const setMenuPins = useMenuStore(state => state.setMenuPins);
const alias = item.type === 'tag' ? db.tags.alias(item.title) : item.title;
const [visible, setVisible] = useState(false);
const [headerTextState, setHeaderTextState] = useState(null);
const color = headerTextState?.id === item.id ? colors.accent : colors.pri;
const fwdRef = useRef();
const onHeaderStateChange = event => {
if (event?.id === item.id) {
setHeaderTextState(event);
} else {
setHeaderTextState(null);
}
};
useEffect(() => {
eSubscribeEvent('onHeaderStateChange', onHeaderStateChange);
return () => {
eUnSubscribeEvent('onHeaderStateChange', onHeaderStateChange);
};
}, []);
const icons = {
topic: 'bookmark',
notebook: 'book-outline',
tag: 'pound'
};
return (
<>
{visible && (
<SheetWrapper
onClose={() => {
setVisible(false);
}}
gestureEnabled={false}
fwdRef={fwdRef}
visible={true}
>
<Seperator />
<Button
title="Remove Shortcut"
type="error"
onPress={async () => {
await db.settings.unpin(item.id);
setVisible(false);
setMenuPins();
}}
fontSize={SIZE.md}
width="95%"
height={50}
customStyle={{
marginBottom: 30
}}
/>
</SheetWrapper>
)}
<PressableButton
type={headerTextState?.id === item.id ? 'grayBg' : 'gray'}
onLongPress={() => {
Properties.present(item);
}}
onPress={() => onPress(item)}
customStyle={{
width: '100%',
alignSelf: 'center',
borderRadius: 5,
flexDirection: 'row',
paddingHorizontal: 8,
justifyContent: 'space-between',
alignItems: 'center',
height: normalize(50),
marginBottom: 5
}}
>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
flexGrow: 1,
flex: 1
}}
>
<View
style={{
width: 30,
justifyContent: 'center'
}}
>
<Icon color={color} size={SIZE.lg - 2} name={icons[item.type]} />
<Icon
style={{
position: 'absolute',
bottom: -6,
left: -6
}}
color={color}
size={SIZE.xs}
name="arrow-top-right-thick"
/>
</View>
<View
style={{
alignItems: 'flex-start',
flexGrow: 1,
flex: 1
}}
>
{headerTextState?.id === item.id ? (
<Heading
style={{
flexWrap: 'wrap'
}}
color={colors.heading}
size={SIZE.md}
>
{alias}
</Heading>
) : (
<Paragraph numberOfLines={1} color={colors.pri} size={SIZE.md}>
{alias}
</Paragraph>
)}
</View>
</View>
</PressableButton>
</>
);
};

View File

@@ -1,22 +0,0 @@
import React, { useEffect, useState } from 'react';
import { timeSince } from '../../utils/TimeUtils';
import Paragraph from '../Typography/Paragraph';
export const TimeSince = ({ time, style, updateFrequency = 30000 }) => {
const [timeAgo, setTimeAgo] = useState(null);
useEffect(() => {
let t = timeSince(time || Date.now());
setTimeAgo(t);
let interval = setInterval(() => {
t = timeSince(time);
setTimeAgo(t);
}, updateFrequency);
return () => {
clearInterval(interval);
interval = null;
};
}, [time, updateFrequency]);
return <Paragraph style={style}>{timeAgo}</Paragraph>;
};

View File

@@ -1,158 +0,0 @@
import React from 'react';
import { FlatList, View } from 'react-native';
import Animated from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { notesnook } from '../../../e2e/test.ids';
import { useTracked } from '../../provider';
import { Actions } from '../../provider/Actions';
import { useSettingStore, useUserStore } from '../../provider/stores';
import { DDS } from '../../services/DeviceDetection';
import { eSendEvent } from '../../services/EventManager';
import { DrawerScale } from '../../utils/Animations';
import {
ACCENT,
COLOR_SCHEME,
COLOR_SCHEME_DARK,
COLOR_SCHEME_LIGHT,
setColorScheme
} from '../../utils/Colors';
import { eOpenPremiumDialog } from '../../utils/Events';
import { MenuItemsList, SUBSCRIPTION_STATUS } from '../../utils/index';
import { MMKV } from '../../utils/mmkv';
import umami from '../../utils/umami';
import { ColorSection } from './ColorSection';
import { MenuListItem } from './MenuListItem';
import { TagsSection } from './TagsSection';
import { UserSection } from './UserSection';
export const Menu = React.memo(
() => {
const [state, dispatch] = useTracked();
const { colors } = state;
const deviceMode = useSettingStore(state => state.deviceMode);
const insets = useSafeAreaInsets();
const user = useUserStore(state => state.user);
const noTextMode = false;
function changeColorScheme(colors = COLOR_SCHEME, accent = ACCENT) {
let newColors = setColorScheme(colors, accent);
dispatch({ type: Actions.THEME, colors: newColors });
}
const BottomItemsList = [
{
name: colors.night ? 'Day' : 'Night',
icon: 'theme-light-dark',
func: () => {
if (!colors.night) {
MMKV.setStringAsync('theme', JSON.stringify({ night: true }));
changeColorScheme(COLOR_SCHEME_DARK);
} else {
MMKV.setStringAsync('theme', JSON.stringify({ night: false }));
changeColorScheme(COLOR_SCHEME_LIGHT);
}
},
switch: true,
on: !!colors.night,
close: false
},
{
name: 'Settings',
icon: 'cog-outline',
close: true
}
];
const pro = {
name: 'Notesnook Pro',
icon: 'crown',
func: () => {
umami.pageView('/pro-screen', '/sidemenu');
eSendEvent(eOpenPremiumDialog);
}
};
return (
<View
style={{
height: '100%',
width: '100%',
backgroundColor: colors.nav
}}
>
<Animated.View
style={{
height: '100%',
width: '100%',
backgroundColor: deviceMode !== 'mobile' ? colors.nav : colors.bg,
paddingTop: insets.top,
borderRadius: 10,
transform: [
{
scale: deviceMode !== 'mobile' ? 1 : DrawerScale
}
]
}}
>
<FlatList
alwaysBounceVertical={false}
contentContainerStyle={{
flexGrow: 1
}}
style={{
height: '100%',
width: '100%',
paddingHorizontal: 12
}}
showsVerticalScrollIndicator={false}
data={[0]}
keyExtractor={() => 'mainMenuView'}
renderItem={() => (
<>
{MenuItemsList.map((item, index) => (
<MenuListItem key={item.name} item={item} testID={item.name} index={index} />
))}
<ColorSection noTextMode={noTextMode} />
<TagsSection />
</>
)}
/>
<View
style={{
paddingHorizontal: 12
}}
>
{!user ||
user?.subscription?.type === SUBSCRIPTION_STATUS.TRIAL ||
user?.subscription?.type === SUBSCRIPTION_STATUS.BASIC ? (
<MenuListItem testID={pro.name} key={pro.name} item={pro} index={0} ignore={true} />
) : null}
{BottomItemsList.slice(DDS.isLargeTablet() ? 0 : 1, 3).map((item, index) => (
<MenuListItem
testID={item.name == 'Night mode' ? notesnook.ids.menu.nightmode : item.name}
key={item.name}
item={item}
index={index}
ignore={true}
rightBtn={
DDS.isLargeTablet() || item.name === 'Notesnook Pro' ? null : BottomItemsList[0]
}
/>
))}
</View>
<View
style={{
width: '100%',
paddingHorizontal: 0
}}
>
<UserSection noTextMode={noTextMode} />
</View>
</Animated.View>
</View>
);
},
() => true
);

View File

@@ -1,119 +0,0 @@
import React from 'react';
import NoteItem from '.';
import { notesnook } from '../../../e2e/test.ids';
import { useSelectionStore, useTrashStore } from '../../provider/stores';
import { DDS } from '../../services/DeviceDetection';
import { eSendEvent, openVault, ToastEvent } from '../../services/EventManager';
import Navigation from '../../services/Navigation';
import { history } from '../../utils';
import { db } from '../../utils/database';
import { eOnLoadNote, eShowMergeDialog } from '../../utils/Events';
import { tabBarRef } from '../../utils/Refs';
import { presentDialog } from '../Dialog/functions';
import SelectionWrapper from '../SelectionWrapper';
export const NoteWrapper = React.memo(
({ item, index, tags, dateBy }) => {
const isTrash = item.type === 'trash';
const setSelectedItem = useSelectionStore(state => state.setSelectedItem);
const onPress = async () => {
let _note = item;
if (!isTrash) {
_note = db.notes.note(item.id).data;
}
if (history.selectedItemsList.length > 0 && history.selectionMode) {
setSelectedItem(_note);
return;
} else {
history.selectedItemsList = [];
}
if (_note.conflicted) {
eSendEvent(eShowMergeDialog, _note);
return;
}
if (_note.locked) {
openVault({
item: _note,
novault: true,
locked: true,
goToEditor: true,
title: 'Open note',
description: 'Unlock note to open it in editor.'
});
return;
}
if (isTrash) {
presentDialog({
title: `Restore ${item.itemType}`,
paragraph: `Restore or delete ${item.itemType} forever`,
positiveText: 'Restore',
negativeText: 'Delete',
positivePress: async () => {
await db.trash.restore(item.id);
Navigation.setRoutesToUpdate([
Navigation.routeNames.Tags,
Navigation.routeNames.Notes,
Navigation.routeNames.Notebooks,
Navigation.routeNames.NotesPage,
Navigation.routeNames.Favorites,
Navigation.routeNames.Trash
]);
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
heading: 'Restore successful',
type: 'success'
});
},
onClose: async () => {
await db.trash.delete(item.id);
useTrashStore.getState().setTrash();
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
heading: 'Permanantly deleted items',
type: 'success',
context: 'local'
});
}
});
} else {
eSendEvent(eOnLoadNote, _note);
if (!DDS.isTab) {
tabBarRef.current?.goToPage(1);
}
}
};
return (
<SelectionWrapper
index={index}
height={100}
testID={notesnook.ids.note.get(index)}
onPress={onPress}
item={item}
>
<NoteItem item={item} dateBy={dateBy} tags={tags} isTrash={isTrash} />
</SelectionWrapper>
);
},
(prev, next) => {
if (prev.dateBy !== next.dateBy) {
return false;
}
if (prev.item?.dateEdited !== next.item?.dateEdited) {
return false;
}
if (JSON.stringify(prev.tags) !== JSON.stringify(next.tags)) {
return false;
}
if (prev.item !== next.item) {
return false;
}
return true;
}
);

View File

@@ -1,462 +0,0 @@
import React, { useEffect, useState } from 'react';
import { ActivityIndicator, Platform, Text, View } from 'react-native';
import * as RNIap from 'react-native-iap';
import { useTracked } from '../../provider';
import { useUserStore } from '../../provider/stores';
import { eSendEvent, presentSheet, ToastEvent } from '../../services/EventManager';
import PremiumService from '../../services/PremiumService';
import { db } from '../../utils/database';
import {
eClosePremiumDialog,
eCloseProgressDialog,
eCloseSimpleDialog,
eOpenLoginDialog
} from '../../utils/Events';
import { openLinkInBrowser } from '../../utils/functions';
import { SIZE } from '../../utils/SizeUtils';
import { sleep } from '../../utils/TimeUtils';
import umami from '../../utils/umami';
import { Button } from '../Button';
import { Dialog } from '../Dialog';
import BaseDialog from '../Dialog/base-dialog';
import { presentDialog } from '../Dialog/functions';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
import { PricingItem } from './pricing-item';
const promoCyclesMonthly = {
1: 'first month',
2: 'first 2 months',
3: 'first 3 months'
};
const promoCyclesYearly = {
1: 'first year',
2: 'first 2 years',
3: 'first 3 years'
};
export const PricingPlans = ({ promo, marginTop, heading = true, compact = false }) => {
const [state, dispatch] = useTracked();
const colors = state.colors;
const user = useUserStore(state => state.user);
const [product, setProduct] = useState(null);
const [products, setProducts] = useState([]);
const [offers, setOffers] = useState(null);
const [buying, setBuying] = useState(false);
const [loading, setLoading] = useState(false);
const getSkus = async () => {
try {
setLoading(true);
let products = await PremiumService.getProducts();
if (products.length > 0) {
let offers = {
monthly: products.find(p => p.productId === 'com.streetwriters.notesnook.sub.mo'),
yearly: products.find(p => p.productId === 'com.streetwriters.notesnook.sub.yr')
};
setOffers(offers);
if (promo?.promoCode) {
getPromo(promo?.promoCode);
}
setProducts(products);
}
setLoading(false);
} catch (e) {
setLoading(false);
console.log('error getting sku', e);
}
};
const getPromo = async code => {
try {
let productId;
if (code.startsWith('com.streetwriters.notesnook')) {
productId = code;
} else {
productId = await db.offers.getCode(code.split(':')[0], Platform.OS);
}
let products = await PremiumService.getProducts();
let product = products.find(p => p.productId === productId);
if (!product) return false;
let isMonthly = product.productId.indexOf('.mo') > -1;
let cycleText = isMonthly
? promoCyclesMonthly[
product.introductoryPriceCyclesAndroid || product.introductoryPriceNumberOfPeriodsIOS
]
: promoCyclesYearly[
product.introductoryPriceCyclesAndroid || product.introductoryPriceNumberOfPeriodsIOS
];
setProduct({
type: 'promo',
offerType: isMonthly ? 'monthly' : 'yearly',
data: product,
cycleText: cycleText,
info: 'Pay monthly, cancel anytime'
});
return true;
} catch (e) {
console.log('PROMOCODE ERROR:', code, e);
return false;
}
};
useEffect(() => {
getSkus();
}, []);
const buySubscription = async product => {
if (buying) return;
setBuying(true);
try {
if (!user) {
setBuying(false);
return;
}
umami.pageView('/iap-native', `${compact ? 'pro-sheet' : 'pro-screen'}/pro-plans`);
await RNIap.requestSubscription(product?.productId, false, null, null, null, user.id);
setBuying(false);
eSendEvent(eCloseProgressDialog);
eSendEvent(eClosePremiumDialog);
await sleep(500);
presentSheet({
title: 'Thank you for subscribing!',
paragraph: `Your Notesnook Pro subscription will be activated within a few hours. If your account is not upgraded to Notesnook Pro, your money will be refunded to you. In case of any issues, please reach out to us at support@streetwriters.co`,
action: async () => {
eSendEvent(eCloseProgressDialog);
},
icon: 'check',
actionText: 'Continue'
});
} catch (e) {
setBuying(false);
console.log(e);
}
};
return loading ? (
<View
style={{
paddingHorizontal: 12,
justifyContent: 'center',
alignItems: 'center',
height: 100
}}
>
<ActivityIndicator color={colors.accent} size={25} />
</View>
) : (
<View
style={{
paddingHorizontal: 12
}}
>
{buying ? (
<BaseDialog statusBarTranslucent centered>
<ActivityIndicator size={50} color="white" />
</BaseDialog>
) : null}
{product?.type === 'promo' ? (
<Heading
style={{
paddingVertical: 15,
alignSelf: 'center',
textAlign: 'center'
}}
size={SIZE.lg - 4}
>
{product.data.introductoryPrice}
<Paragraph
style={{
textDecorationLine: 'line-through',
color: colors.icon
}}
size={SIZE.sm}
>
({product.data.localizedPrice})
</Paragraph>{' '}
for {product.cycleText}
</Heading>
) : null}
{user && !product ? (
<>
{heading ? (
<Heading
style={{
alignSelf: 'center',
marginTop: marginTop || 20,
marginBottom: 20
}}
>
Choose a plan
</Heading>
) : null}
<View
style={{
flexDirection: !compact ? 'column' : 'row',
flexWrap: 'wrap',
justifyContent: 'space-around'
}}
>
<PricingItem
onPress={() => buySubscription(offers?.monthly)}
compact={compact}
product={{
type: 'monthly',
data: offers?.monthly,
info: 'Pay monthly, cancel anytime.'
}}
/>
{!compact && (
<View
style={{
height: 1,
marginVertical: 5
}}
/>
)}
<PricingItem
onPress={() => buySubscription(offers?.yearly)}
compact={compact}
product={{
type: 'yearly',
data: offers?.yearly,
info: 'Pay yearly'
}}
/>
</View>
{Platform.OS !== 'ios' ? (
<Button
height={35}
style={{
marginTop: 10
}}
onPress={() => {
presentDialog({
context: 'local',
input: true,
inputPlaceholder: 'Enter code',
positiveText: 'Apply',
positivePress: async value => {
if (!value) return;
console.log(value);
eSendEvent(eCloseSimpleDialog);
setBuying(true);
try {
if (!(await getPromo(value))) throw new Error('Error applying promo code');
ToastEvent.show({
heading: 'Discount applied!',
type: 'success',
context: 'local'
});
setBuying(false);
} catch (e) {
setBuying(false);
ToastEvent.show({
heading: 'Promo code invalid or expired',
message: e.message,
type: 'error',
context: 'local'
});
}
},
title: 'Have a promo code?',
paragraph: 'Enter your promo code to get a special discount.'
});
}}
title="I have a promo code"
/>
) : (
<View
style={{
height: 15
}}
/>
)}
</>
) : (
<View>
{!user ? (
<>
<Button
onPress={() => {
eSendEvent(eClosePremiumDialog);
eSendEvent(eCloseProgressDialog);
setTimeout(() => {
eSendEvent(eOpenLoginDialog, 1);
}, 400);
}}
title={`Sign up for free`}
type="accent"
style={{
paddingHorizontal: 24,
marginTop: 20,
marginBottom: 10
}}
/>
{Platform.OS !== 'ios' &&
promo &&
!promo.promoCode.startsWith('com.streetwriters.notesnook') ? (
<Paragraph
size={SIZE.md}
textBreakStrategy="balanced"
style={{
alignSelf: 'center',
justifyContent: 'center',
textAlign: 'center'
}}
>
Use promo code{' '}
<Text
style={{
fontFamily: 'OpenSans-SemiBold'
}}
>
{promo.promoCode}
</Text>{' '}
at checkout
</Paragraph>
) : null}
</>
) : (
<>
<Button
onPress={() => buySubscription(product.data)}
height={40}
width="50%"
type="accent"
title="Subscribe now"
/>
<Button
onPress={() => {
setProduct(null);
}}
style={{
marginTop: 5
}}
height={30}
fontSize={13}
type="errorShade"
title="Cancel promo code"
/>
</>
)}
</View>
)}
{!user ? (
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
alignSelf: 'center',
textAlign: 'center',
marginTop: 10,
maxWidth: '80%'
}}
>
Your 14 day free trial will activate when you sign up.{' '}
<Paragraph size={SIZE.xs} style={{ fontWeight: 'bold' }}>
No credit card is required.
</Paragraph>
</Paragraph>
) : null}
{user ? (
<>
{Platform.OS === 'ios' ? (
<Paragraph
textBreakStrategy="balanced"
size={SIZE.xs}
color={colors.icon}
style={{
alignSelf: 'center',
marginTop: 10,
textAlign: 'center'
}}
>
By subscribing, you will be charged to your iTunes Account for the selected plan.
Subscriptions will automatically renew unless cancelled within 24-hours before the end
of the current period.
</Paragraph>
) : (
<Paragraph
size={SIZE.xs}
color={colors.icon}
style={{
alignSelf: 'center',
marginTop: 10,
textAlign: 'center'
}}
>
By subscribing, your will be charged on your Google Account, and your subscription
will automatically renew until you cancel prior to the end of the then current period.
</Paragraph>
)}
<View
style={{
width: '100%'
}}
>
<Paragraph
size={SIZE.xs}
color={colors.icon}
style={{
maxWidth: '100%',
textAlign: 'center'
}}
>
By subscribing, you agree to our{' '}
<Paragraph
size={SIZE.xs}
onPress={() => {
openLinkInBrowser('https://notesnook.com/tos', colors)
.catch(e => {})
.then(r => {
console.log('closed');
});
}}
style={{
textDecorationLine: 'underline'
}}
color={colors.accent}
>
Terms of Service{' '}
</Paragraph>
and{' '}
<Paragraph
size={SIZE.xs}
onPress={() => {
openLinkInBrowser('https://notesnook.com/privacy', colors)
.catch(e => {})
.then(r => {
console.log('closed');
});
}}
style={{
textDecorationLine: 'underline'
}}
color={colors.accent}
>
Privacy Policy.
</Paragraph>
</Paragraph>
</View>
</>
) : null}
<Dialog context="local" />
</View>
);
};

View File

@@ -1,258 +0,0 @@
import React from 'react';
import { Dimensions, StyleSheet, View } from 'react-native';
const Circle = ({ size, color, position }) => {
let style = {
wrapper: {
flexDirection: 'row',
...position
},
circle: {
width: size,
height: size,
borderRadius: size / 2,
backgroundColor: color
}
};
return (
<View style={style.wrapper}>
<View style={style.circle} />
</View>
);
};
const Donut = ({ size, color, position }) => {
let style = {
wrapper: {
flexDirection: 'row',
...position
},
donut: {
width: size,
height: size,
borderRadius: size / 2,
borderWidth: size / 4,
borderColor: color
}
};
return (
<View style={style.wrapper}>
<View style={style.donut} />
</View>
);
};
const Triangle = ({ size, color, position }) => {
let style = {
wrapper: {
flexDirection: 'row',
...position
},
triangle: {
width: 0,
height: 0,
backgroundColor: 'transparent',
borderStyle: 'solid',
borderLeftWidth: size / 2,
borderRightWidth: size / 2,
borderBottomWidth: size,
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: color,
transform: [{ rotate: '180deg' }]
}
};
return (
<View style={style.wrapper}>
<View style={style.triangle} />
</View>
);
};
const DiamondNarrow = ({ size, color, position }) => {
let style = {
wrapper: {
flexDirection: 'row',
...position
},
diamondNarrow: {},
diamondNarrowTop: {
width: 0,
height: 0,
borderTopWidth: 0,
borderTopColor: 'transparent',
borderLeftColor: 'transparent',
borderLeftWidth: size / 2,
borderRightColor: 'transparent',
borderRightWidth: size / 2,
borderBottomColor: color,
borderBottomWidth: size / 1.42
},
diamondNarrowBottom: {
width: 0,
height: 0,
borderTopWidth: size / 1.42,
borderTopColor: color,
borderLeftColor: 'transparent',
borderLeftWidth: size / 2,
borderRightColor: 'transparent',
borderRightWidth: size / 2,
borderBottomColor: 'transparent',
borderBottomWidth: 0
}
};
return (
<View style={style.wrapper}>
<View style={style.diamondNarrow}>
<View style={style.diamondNarrowTop} />
<View style={style.diamondNarrowBottom} />
</View>
</View>
);
};
const CutDiamond = ({ size, color, position }) => {
let style = {
wrapper: {
flexDirection: 'row',
...position
},
cutDiamond: {},
cutDiamondTop: {
width: size,
height: 0,
borderTopWidth: 0,
borderTopColor: 'transparent',
borderLeftColor: 'transparent',
borderLeftWidth: size / 4,
borderRightColor: 'transparent',
borderRightWidth: size / 4,
borderBottomColor: color,
borderBottomWidth: size / 4
},
cutDiamondBottom: {
width: 0,
height: 0,
borderTopWidth: size / 1.42,
borderTopColor: color,
borderLeftColor: 'transparent',
borderLeftWidth: size / 2,
borderRightColor: 'transparent',
borderRightWidth: size / 2,
borderBottomColor: 'transparent',
borderBottomWidth: 0
}
};
return (
<View style={style.wrapper}>
<View style={style.cutDiamond}>
<View style={style.cutDiamondTop} />
<View style={style.cutDiamondBottom} />
</View>
</View>
);
};
const Shapes = ({ primaryColor, secondaryColor, height, figures, borderRadius, style }) => {
const config = {
primaryColor: primaryColor || '#416DF8',
secondaryColor: secondaryColor || '#2F53D5',
height: Dimensions.get('window').height / (height || 3.5),
sizefigure: 100,
figures: figures || [
{ name: 'circle', position: 'center', size: 60 },
{ name: 'donut', position: 'flex-start', axis: 'top', size: 80 },
{ name: 'circle', position: 'center', axis: 'right', size: 100 }
],
borderRadius: borderRadius !== undefined ? borderRadius : 30
};
const arrFigures = [];
const buildFigures = () => {
config.figures.forEach((e, i) => {
let position = {
alignItems: e.position
};
const sizefigure = e.size || config.sizefigure;
switch (e.axis) {
case 'left':
position.left = -sizefigure / 2;
break;
case 'right':
position.right = -sizefigure / 2;
break;
case 'top':
position.top = -sizefigure / 2;
break;
case 'bottom':
position.bottom = -sizefigure / 2;
break;
default:
break;
}
if (e.name === 'circle') {
arrFigures.push(
<Circle key={i} size={sizefigure} color={config.secondaryColor} position={position} />
);
}
if (e.name === 'donut') {
arrFigures.push(
<Donut key={i} size={sizefigure} color={config.secondaryColor} position={position} />
);
}
if (e.name === 'triangle') {
arrFigures.push(
<Triangle key={i} size={sizefigure} color={config.secondaryColor} position={position} />
);
}
if (e.name === 'diamondNarrow') {
arrFigures.push(
<DiamondNarrow
key={i}
size={sizefigure}
color={config.secondaryColor}
position={position}
/>
);
}
if (e.name === 'cutDiamond') {
arrFigures.push(
<CutDiamond key={i} size={sizefigure} color={config.secondaryColor} position={position} />
);
}
});
return arrFigures;
};
return (
<View
style={{
...styles.wrapper,
backgroundColor: config.primaryColor,
height: config.height,
borderBottomLeftRadius: config.borderRadius,
borderBottomRightRadius: config.borderRadius,
...style
}}
>
<>{buildFigures()}</>
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
position: 'absolute',
height: '100%',
left: 0,
right: 0,
flexDirection: 'row',
justifyContent: 'space-between'
}
});
export { Shapes };

View File

@@ -1,87 +0,0 @@
import React from 'react';
import { ActivityIndicator, useWindowDimensions, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { notesnook } from '../../../e2e/test.ids';
import { useTracked } from '../../provider';
import { COLORS_NOTE } from '../../utils/Colors';
import { normalize, SIZE } from '../../utils/SizeUtils';
import { Button } from '../Button';
import { Placeholder } from '../ListPlaceholders';
import Seperator from '../Seperator';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
export const Empty = ({ loading = true, placeholderData, absolute, headerProps, type, screen }) => {
const [state] = useTracked();
const { colors } = state;
const insets = useSafeAreaInsets();
const { height } = useWindowDimensions();
return (
<View
style={[
{
backgroundColor: colors.bg,
position: absolute ? 'absolute' : 'relative',
zIndex: absolute ? 10 : null,
height: height - 250 - insets.top,
width: '100%'
}
]}
>
<View
style={{
flexGrow: 1,
justifyContent: 'center',
alignItems: 'center'
}}
>
<Placeholder
color={COLORS_NOTE[headerProps.color?.toLowerCase()] || colors.accent}
w={normalize(150)}
h={normalize(150)}
type={screen === 'Favorites' ? 'favorites' : type}
/>
<Heading>{placeholderData.heading}</Heading>
<Paragraph
textBreakStrategy="balanced"
style={{
textAlign: 'center',
width: '80%'
}}
color={colors.icon}
>
{loading ? placeholderData.loading : placeholderData.paragraph}
</Paragraph>
<Seperator />
{placeholderData.button && !loading ? (
<Button
onPress={placeholderData.action}
title={placeholderData.button}
icon={placeholderData.buttonIcon || 'plus'}
testID={notesnook.buttons.add}
type="accent"
fontSize={SIZE.md}
accentColor="bg"
accentText={
COLORS_NOTE[headerProps.color?.toLowerCase()] ? headerProps.color : 'accent'
}
/>
) : loading ? (
<ActivityIndicator
style={{
height: 35
}}
color={COLORS_NOTE[headerProps.color?.toLowerCase()] || colors.accent}
/>
) : (
<View
style={{
height: 35
}}
/>
)}
</View>
</View>
);
};

View File

@@ -1,117 +0,0 @@
import React from 'react';
import { View } from 'react-native';
import { useTracked } from '../../provider';
import { useMessageStore } from '../../provider/stores';
import { COLORS_NOTE } from '../../utils/Colors';
import { hexToRGBA } from '../../utils/ColorUtils';
import { normalize, SIZE } from '../../utils/SizeUtils';
import { Announcement } from '../Announcements/announcement';
import { Button } from '../Button';
import { Placeholder } from '../ListPlaceholders';
import Heading from '../Typography/Heading';
import { Card } from './card';
export const Header = React.memo(
({
type,
messageCard = true,
title,
paragraph,
color,
onPress,
shouldShow = false,
icon,
screen,
noAnnouncement,
height
}) => {
const [state] = useTracked();
const { colors } = state;
const announcements = useMessageStore(state => state.announcements);
return announcements.length !== 0 && !noAnnouncement ? (
<Announcement color={color || colors.accent} />
) : type === 'search' ? null : !shouldShow ? (
<View
style={{
marginBottom: 5,
padding: 0,
width: '100%',
justifyContent: 'center',
alignItems: 'center'
}}
>
{messageCard ? <Card color={COLORS_NOTE[color?.toLowerCase()] || colors.accent} /> : null}
</View>
) : (
<View
style={{
width: '100%'
}}
>
<View
style={{
minHeight: height || 195,
padding: 12,
width: '100%',
zIndex: 10,
justifyContent: 'flex-end',
backgroundColor: COLORS_NOTE[color?.toLowerCase()]
? hexToRGBA(COLORS_NOTE[color?.toLowerCase()], 0.15)
: color || colors.shade
}}
>
<View
style={{
right: 0,
paddingRight: 12,
bottom: 0,
position: 'absolute'
}}
>
<Placeholder
color={COLORS_NOTE[color?.toLowerCase()] || colors.accent}
w={normalize(150)}
h={normalize(150)}
type={screen === 'Favorites' ? 'favorites' : type}
/>
</View>
<View
style={{
marginTop: 15
}}
>
<Heading style={{ marginBottom: paragraph ? 0 : 0 }} size={SIZE.xxxl * 1.2}>
<Heading size={SIZE.xxxl * 1.2} color={colors.accent}>
{title.slice(0, 1) === '#' ? '#' : null}
</Heading>
{title.slice(0, 1) === '#' ? title.slice(1) : title}
</Heading>
{paragraph ? (
<Button
height={20}
title={paragraph}
icon={icon}
style={{
alignSelf: 'flex-start',
paddingLeft: 0
}}
textStyle={{
fontWeight: 'normal'
}}
iconSize={SIZE.sm}
fontSize={SIZE.sm}
onPress={onPress}
/>
) : null}
</View>
</View>
</View>
);
}
);
Header.displayName = 'Header';

View File

@@ -1,134 +0,0 @@
import React, { useEffect, useState } from 'react';
import { TouchableOpacity, useWindowDimensions, View } from 'react-native';
import { useTracked } from '../../provider';
import { useSettingStore } from '../../provider/stores';
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
presentSheet
} from '../../services/EventManager';
import SettingsService from '../../services/SettingsService';
import { GROUP } from '../../utils';
import { COLORS_NOTE } from '../../utils/Colors';
import { db } from '../../utils/database';
import { eOpenJumpToDialog } from '../../utils/Events';
import { SIZE } from '../../utils/SizeUtils';
import { ActionIcon } from '../ActionIcon';
import { Button } from '../Button';
import Sort from '../Sort';
import Heading from '../Typography/Heading';
export const SectionHeader = ({ item, index, type, color, screen }) => {
const [state] = useTracked();
const { colors } = state;
const { fontScale } = useWindowDimensions();
const [groupOptions, setGroupOptions] = useState(db.settings?.getGroupOptions(type));
let groupBy = Object.keys(GROUP).find(key => GROUP[key] === groupOptions.groupBy);
const settings = useSettingStore(state => state.settings);
const listMode = type === 'notebooks' ? settings.notebooksListMode : settings.notesListMode;
groupBy = !groupBy
? 'Default'
: groupBy.slice(0, 1).toUpperCase() + groupBy.slice(1, groupBy.length);
const onUpdate = () => {
setGroupOptions({ ...db.settings?.getGroupOptions(type) });
};
useEffect(() => {
eSubscribeEvent('groupOptionsUpdate', onUpdate);
return () => {
eUnSubscribeEvent('groupOptionsUpdate', onUpdate);
};
}, []);
return (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
width: '95%',
justifyContent: 'space-between',
paddingHorizontal: 12,
height: 35 * fontScale,
backgroundColor: colors.nav,
alignSelf: 'center',
borderRadius: 5,
marginVertical: 5
}}
>
<TouchableOpacity
onPress={() => {
eSendEvent(eOpenJumpToDialog, type);
}}
activeOpacity={0.9}
hitSlop={{ top: 10, left: 10, right: 30, bottom: 15 }}
style={{
height: '100%',
justifyContent: 'center'
}}
>
<Heading
color={COLORS_NOTE[color?.toLowerCase()] || colors.accent}
size={SIZE.sm}
style={{
minWidth: 60,
alignSelf: 'center',
textAlignVertical: 'center'
}}
>
{!item.title || item.title === '' ? 'Pinned' : item.title}
</Heading>
</TouchableOpacity>
<View
style={{
flexDirection: 'row',
alignItems: 'center'
}}
>
{index === 0 ? (
<>
<Button
onPress={() => {
presentSheet({
component: <Sort screen={screen} type={type} />
});
}}
title={groupBy}
icon={groupOptions.sortDirection === 'asc' ? 'sort-ascending' : 'sort-descending'}
height={25}
style={{
borderRadius: 100,
paddingHorizontal: 0,
backgroundColor: 'transparent',
marginRight: type === 'notes' || type === 'home' || type === 'notebooks' ? 10 : 0
}}
type="gray"
iconPosition="right"
/>
{type === 'notes' || type === 'notebooks' || type === 'home' ? (
<ActionIcon
customStyle={{
width: 25,
height: 25
}}
color={colors.icon}
name={listMode == 'compact' ? 'view-list' : 'view-list-outline'}
onPress={() => {
SettingsService.set(
type !== 'notebooks' ? 'notesListMode' : 'notebooksListMode',
listMode === 'normal' ? 'compact' : 'normal'
);
}}
size={SIZE.lg - 2}
/>
) : null}
</>
) : null}
</View>
</View>
);
};

View File

@@ -1,40 +0,0 @@
import React from 'react';
import { Platform } from 'react-native';
import { Text } from 'react-native';
import { useTracked } from '../../provider';
import { SIZE } from '../../utils/SizeUtils';
/**
*
* @typedef {import('react-native').TextProps} TextType
* @typedef {Object} restTypes
* @property {string} color color
* @property {number} size color
*/
/**
*
* @param {TextType | restTypes} props all props
*/
const Heading = ({ color, size = SIZE.xl, style, ...restProps }) => {
const [state] = useTracked();
const { colors } = state;
return (
<Text
allowFontScaling={true}
maxFontSizeMultiplier={1}
{...restProps}
style={[
{
fontSize: size || SIZE.xl,
color: color || colors.heading,
fontFamily: Platform.OS === 'android' ? 'OpenSans-SemiBold' : null,
fontWeight: Platform.OS === 'ios' ? '600' : null
},
style
]}
></Text>
);
};
export default Heading;

View File

@@ -1,15 +1,12 @@
import React from 'react';
import { FlatList, View } from 'react-native';
import { useTracked } from '../../provider';
import { useMessageStore, useSelectionStore } from '../../provider/stores';
import { Button } from '../Button';
import { useMessageStore, useSelectionStore } from '../../stores/stores';
import { useThemeStore } from '../../stores/theme';
import { allowedOnPlatform, renderItem } from './functions';
export const Announcement = ({ color }) => {
const [state] = useTracked();
const colors = state.colors;
const colors = useThemeStore(state => state.colors);
const announcements = useMessageStore(state => state.announcements);
const remove = useMessageStore(state => state.remove);
let announcement = announcements.length > 0 ? announcements[0] : null;
const selectionMode = useSelectionStore(state => state.selectionMode);
@@ -17,38 +14,25 @@ export const Announcement = ({ color }) => {
<View
style={{
backgroundColor: colors.bg,
width: '100%'
width: '100%',
paddingHorizontal: 12,
paddingTop: 12,
paddingBottom: 12
}}
>
<View
style={{
paddingVertical: 12,
width: '100%',
borderRadius: 10,
overflow: 'hidden'
overflow: 'hidden',
backgroundColor: colors.nav
}}
>
<Button
type="errorShade"
icon="close"
height={null}
onPress={() => {
remove(announcement.id);
}}
iconSize={22}
style={{
borderRadius: 100,
paddingHorizontal: 0,
position: 'absolute',
top: 10,
right: 10
}}
/>
<View>
<FlatList
style={{
width: '100%',
marginTop: 15
marginTop: 12
}}
data={announcement?.body.filter(item => allowedOnPlatform(item.platforms))}
renderItem={({ item, index }) =>

View File

@@ -1,11 +1,10 @@
import React from 'react';
import { useTracked } from '../../provider';
import Paragraph from '../Typography/Paragraph';
import { useThemeStore } from '../../stores/theme';
import Paragraph from '../ui/typography/paragraph';
import { getStyle } from './functions';
export const Body = ({ text, style = {} }) => {
const [state] = useTracked();
const colors = state.colors;
const colors = useThemeStore(state => state.colors);
return (
<Paragraph

View File

@@ -0,0 +1,146 @@
import React from 'react';
import { Linking, View } from 'react-native';
import SettingsBackupAndRestore from '../../screens/settings/backup-restore';
import { eSendEvent, presentSheet } from '../../services/event-manager';
import { useThemeStore } from '../../stores/theme';
import { eCloseAnnouncementDialog } from '../../utils/events';
import { SIZE } from '../../utils/size';
import { sleep } from '../../utils/time';
import { PricingPlans } from '../premium/pricing-plans';
import SheetProvider from '../sheet-provider';
import { Button } from '../ui/button';
import { allowedOnPlatform, getStyle } from './functions';
export const Cta = ({ actions, style = {}, color, inline }) => {
const colors = useThemeStore(state => state.colors);
let buttons = actions.filter(item => allowedOnPlatform(item.platforms)) || [];
const onPress = async item => {
if (!inline) {
eSendEvent(eCloseAnnouncementDialog);
await sleep(500);
}
if (item.type === 'link') {
Linking.openURL(item.data).catch(console.log);
} else if (item.type === 'promo') {
presentSheet({
component: (
<PricingPlans
marginTop={1}
promo={{
promoCode: item.data,
text: item.title
}}
/>
)
});
} else if (item.type === 'backup') {
presentSheet({
title: 'Backup & restore',
paragraph: 'Please enable automatic backups to keep your data safe',
component: <SettingsBackupAndRestore isSheet={true} />
});
}
};
return (
<View
style={{
paddingHorizontal: 12,
...getStyle(style),
flexDirection: inline ? 'row' : 'column'
}}
>
<SheetProvider context="premium_cta" />
{inline ? (
<>
{buttons.length > 0 &&
buttons.slice(0, 1).map(item => (
<Button
key={item.title}
title={item.title}
fontSize={SIZE.sm}
type="transparent"
textStyle={{
textDecorationLine: 'underline'
}}
onPress={() => onPress(item)}
bold
style={{
height: 30,
alignSelf: 'flex-start',
paddingHorizontal: 0,
marginTop: -6
}}
/>
))}
{buttons.length > 1 &&
buttons.slice(1, 2).map((item, index) => (
<Button
key={item.title}
title={item.title}
fontSize={SIZE.sm}
type="gray"
onPress={() => onPress(item)}
width={null}
height={30}
style={{
alignSelf: 'flex-start',
paddingHorizontal: 0,
marginTop: -6,
marginLeft: 12
}}
textStyle={{
textDecorationLine: 'underline'
}}
/>
))}
</>
) : (
<>
{buttons.length > 0 &&
buttons.slice(0, 1).map(item => (
<Button
key={item.title}
title={item.title}
fontSize={SIZE.md}
buttonType={{
color: color ? color : colors.accent,
text: colors.light,
selected: color ? color : colors.accent,
opacity: 1
}}
onPress={() => onPress(item)}
width={250}
style={{
marginBottom: 5,
borderRadius: 100
}}
/>
))}
{buttons.length > 1 &&
buttons.slice(1, 2).map((item, index) => (
<Button
key={item.title}
title={item.title}
fontSize={SIZE.xs + 1}
type="gray"
onPress={() => onPress(item)}
width={null}
height={30}
style={{
minWidth: '50%',
marginTop: 5
}}
textStyle={{
textDecorationLine: 'underline'
}}
/>
))}
</>
)}
</View>
);
};

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { useThemeStore } from '../../stores/theme';
import { SIZE } from '../../utils/size';
import Paragraph from '../ui/typography/paragraph';
import { getStyle } from './functions';
export const Description = ({ text, style = {}, inline }) => {
const colors = useThemeStore(state => state.colors);
return (
<Paragraph
style={{
marginHorizontal: 12,
...getStyle(style),
textAlign: inline ? 'left' : style?.textAlign
}}
size={inline ? SIZE.sm : SIZE.md}
>
{text}
</Paragraph>
);
};

View File

@@ -1,7 +1,7 @@
import React, { Fragment } from 'react';
import { View } from 'react-native';
import { allowedPlatforms } from '../../provider/stores';
import { ProFeatures } from '../ResultDialog/pro-features';
import { allowedPlatforms } from '../../stores/stores';
import { ProFeatures } from '../dialogs/result/pro-features';
import { Body } from './body';
import { Cta } from './cta';
import { Description } from './description';

View File

@@ -1,16 +1,15 @@
import React, { useEffect, useState } from 'react';
import { FlatList, View } from 'react-native';
import { useTracked } from '../../provider';
import { useMessageStore } from '../../provider/stores';
import { DDS } from '../../services/DeviceDetection';
import { eSubscribeEvent, eUnSubscribeEvent } from '../../services/EventManager';
import { eCloseAnnouncementDialog, eOpenAnnouncementDialog } from '../../utils/Events';
import BaseDialog from '../Dialog/base-dialog';
import { useThemeStore } from '../../stores/theme';
import { useMessageStore } from '../../stores/stores';
import { DDS } from '../../services/device-detection';
import { eSubscribeEvent, eUnSubscribeEvent } from '../../services/event-manager';
import { eCloseAnnouncementDialog, eOpenAnnouncementDialog } from '../../utils/events';
import BaseDialog from '../dialog/base-dialog';
import { allowedOnPlatform, renderItem } from './functions';
export const AnnouncementDialog = () => {
const [state] = useTracked();
const colors = state.colors;
const colors = useThemeStore(state => state.colors);
const [visible, setVisible] = useState(false);
const [info, setInfo] = useState(null);
const remove = useMessageStore(state => state.remove);
@@ -52,7 +51,9 @@ export const AnnouncementDialog = () => {
maxHeight: DDS.isTab ? '90%' : '100%',
borderRadius: DDS.isTab ? 10 : 0,
overflow: 'hidden',
marginBottom: DDS.isTab ? 20 : 0
marginBottom: DDS.isTab ? 20 : 0,
borderTopRightRadius: 10,
borderTopLeftRadius: 10
}}
>
<FlatList

View File

@@ -1,13 +1,12 @@
import React from 'react';
import { View } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { useTracked } from '../../provider';
import Paragraph from '../Typography/Paragraph';
import { useThemeStore } from '../../stores/theme';
import Paragraph from '../ui/typography/paragraph';
import { getStyle } from './functions';
export const List = ({ items, listType, style = {} }) => {
const [state] = useTracked();
const colors = state.colors;
const colors = useThemeStore(state => state.colors);
return (
<View

View File

@@ -1,12 +1,11 @@
import React from 'react';
import { useTracked } from '../../provider';
import { SIZE } from '../../utils/SizeUtils';
import Heading from '../Typography/Heading';
import { useThemeStore } from '../../stores/theme';
import { SIZE } from '../../utils/size';
import Heading from '../ui/typography/heading';
import { getStyle } from './functions';
export const SubHeading = ({ text, style = {} }) => {
const [state] = useTracked();
const colors = state.colors;
const colors = useThemeStore(state => state.colors);
return (
<Heading

View File

@@ -0,0 +1,72 @@
import React from 'react';
import { View } from 'react-native';
import { useMessageStore } from '../../stores/stores';
import { SIZE } from '../../utils/size';
import { Button } from '../ui/button';
import Heading from '../ui/typography/heading';
import { getStyle } from './functions';
export const Title = ({ text, style = {}, inline }) => {
const announcements = useMessageStore(state => state.announcements);
let announcement = announcements.length > 0 ? announcements[0] : null;
const remove = useMessageStore(state => state.remove);
return inline ? (
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: inline ? 5 : 0
}}
>
<Heading
style={{
marginHorizontal: 12,
marginTop: 12,
...getStyle(style),
textAlign: inline ? 'left' : style?.textAlign,
flexShrink: 1
}}
numberOfLines={1}
size={inline ? SIZE.md : SIZE.xl}
>
{inline ? text?.toUpperCase() : text}
</Heading>
<Button
type="gray"
icon="close"
height={null}
onPress={() => {
remove(announcement.id);
}}
hitSlop={{
left: 15,
top: 10,
bottom: 10,
right: 0
}}
iconSize={24}
fontSize={SIZE.xs + 1}
style={{
borderRadius: 100,
paddingVertical: 0,
paddingHorizontal: 0,
marginRight: 12,
zIndex: 10
}}
/>
</View>
) : (
<Heading
style={{
marginHorizontal: 12,
...getStyle(style),
marginTop: style?.marginTop || 12
}}
>
{text}
</Heading>
);
};

View File

@@ -0,0 +1,327 @@
import Clipboard from '@react-native-clipboard/clipboard';
import React, { useEffect, useState } from 'react';
import { View } from 'react-native';
import { ScrollView } from 'react-native-gesture-handler';
import picker from '../../screens/editor/tiny/toolbar/picker';
import { eSendEvent, presentSheet, ToastEvent } from '../../services/event-manager';
import PremiumService from '../../services/premium';
import { useAttachmentStore } from '../../stores/stores';
import { useThemeStore } from '../../stores/theme';
import { formatBytes } from '../../utils';
import { db } from '../../utils/database';
import { eCloseAttachmentDialog, eCloseProgressDialog } from '../../utils/events';
import filesystem from '../../utils/filesystem';
import { useAttachmentProgress } from '../../utils/hooks/use-attachment-progress';
import { SIZE } from '../../utils/size';
import { sleep } from '../../utils/time';
import { Dialog } from '../dialog';
import { presentDialog } from '../dialog/functions';
import { openNote } from '../list-items/note/wrapper';
import { DateMeta } from '../properties/date-meta';
import { Button } from '../ui/button';
import { Notice } from '../ui/notice';
import { PressableButton } from '../ui/pressable';
import Heading from '../ui/typography/heading';
import Paragraph from '../ui/typography/paragraph';
const Actions = ({ attachment, setAttachments, fwdRef }) => {
const colors = useThemeStore(state => state.colors);
const contextId = attachment.metadata.hash;
const [filename, setFilename] = useState(attachment.metadata.filename);
const [currentProgress, setCurrentProgress] = useAttachmentProgress(attachment);
const [failed, setFailed] = useState(attachment.failed);
const [notes, setNotes] = useState([]);
const [loading, setLoading] = useState({
name: null
});
const actions = [
{
name: 'Download',
onPress: async () => {
if (currentProgress) {
await db.fs.cancel(attachment.metadata.hash, 'download');
useAttachmentStore.getState().remove(attachment.metadata.hash);
}
filesystem.downloadAttachment(attachment.metadata.hash, false);
eSendEvent(eCloseProgressDialog, contextId);
},
icon: 'download'
},
{
name: 'Reupload',
onPress: async () => {
if (!PremiumService.get()) {
ToastEvent.show({
heading: 'Upgrade to pro',
type: 'error',
context: 'local'
});
return;
}
await picker.pick({
reupload: true,
hash: attachment.metadata.hash,
context: contextId,
type: attachment.metadata.type
});
},
icon: 'upload'
},
{
name: 'Run file check',
onPress: async () => {
setLoading({
name: 'Run file check'
});
let res = await filesystem.checkAttachment(attachment.metadata.hash);
if (res.failed) {
db.attachments.markAsFailed(attachment.id, res.failed);
setFailed(res.failed);
} else {
setFailed(null);
db.attachments.markAsFailed(attachment.id, null);
}
ToastEvent.show({
heading: 'File check passed',
type: 'success',
context: 'local'
});
setAttachments([...db.attachments.all]);
setLoading({
name: null
});
},
icon: 'file-check'
},
{
name: 'Rename',
onPress: () => {
presentDialog({
context: contextId,
input: true,
title: 'Rename file',
paragraph: 'Enter a new name for the file',
defaultValue: attachment.metadata.filename,
positivePress: async value => {
if (value && value.length > 0) {
await db.attachments.add({
hash: attachment.metadata.hash,
filename: value
});
setFilename(value);
setAttachments([...db.attachments.all]);
}
},
positiveText: 'Rename'
});
},
icon: 'form-textbox'
},
{
name: 'Delete',
onPress: async () => {
await db.attachments.remove(attachment.metadata.hash, false);
setAttachments([...db.attachments.all]);
eSendEvent(eCloseProgressDialog, contextId);
},
icon: 'delete-outline'
}
];
const getNotes = () => {
let allNotes = db.notes.all;
let attachmentNotes = attachment.noteIds?.map(id => {
let index = allNotes?.findIndex(note => id === note.id);
if (index !== -1) {
return allNotes[index];
} else {
return {
type: 'notfound',
title: `Note with id ${id} does not exist.`,
id: id
};
}
});
return attachmentNotes;
};
useEffect(() => {
setNotes(getNotes());
}, [attachment]);
return (
<ScrollView
onMomentumScrollEnd={() => {
fwdRef?.current?.handleChildScrollEnd();
}}
nestedScrollEnabled={true}
style={{
maxHeight: '100%'
}}
>
<Dialog context={contextId} />
<View
style={{
borderBottomWidth: 1,
borderBottomColor: colors.nav,
marginBottom: notes && notes.length > 0 ? 0 : 12
}}
>
<Heading
style={{
paddingHorizontal: 12
}}
size={SIZE.lg}
>
{filename}
</Heading>
<View
style={{
flexDirection: 'row',
marginBottom: 10,
paddingHorizontal: 12
}}
>
<Paragraph
size={SIZE.xs + 1}
style={{
marginRight: 10
}}
color={colors.icon}
>
{attachment.metadata.type}
</Paragraph>
<Paragraph
style={{
marginRight: 10
}}
size={SIZE.xs + 1}
color={colors.icon}
>
{formatBytes(attachment.length)}
</Paragraph>
<Paragraph
style={{
marginRight: 10
}}
size={SIZE.xs + 1}
color={colors.icon}
>
{attachment.noteIds.length} note{attachment.noteIds.length > 1 ? 's' : ''}
</Paragraph>
<Paragraph
onPress={() => {
Clipboard.setString(attachment.metadata.hash);
ToastEvent.show({
type: 'success',
heading: 'Attachment hash copied',
context: 'local'
});
}}
size={SIZE.xs + 1}
color={colors.icon}
>
{attachment.metadata.hash}
</Paragraph>
</View>
<DateMeta item={attachment} />
</View>
{notes && notes.length > 0 ? (
<View
style={{
borderBottomWidth: 1,
borderBottomColor: colors.nav,
marginBottom: 12,
paddingVertical: 12
}}
>
<>
<Heading
style={{
paddingHorizontal: 12
}}
size={SIZE.sm}
>
List of notes:
</Heading>
{notes.map(item => (
<PressableButton
onPress={async () => {
if (item.type === 'notfound') return;
eSendEvent(eCloseProgressDialog, contextId);
await sleep(150);
eSendEvent(eCloseAttachmentDialog);
await sleep(300);
openNote(item, item.type === 'trash');
}}
customStyle={{
paddingVertical: 12,
alignItems: 'flex-start',
paddingHorizontal: 12
}}
key={item.id}
>
<Paragraph size={SIZE.xs + 1}>{item.title}</Paragraph>
</PressableButton>
))}
</>
</View>
) : null}
{actions.map(item => (
<Button
key={item.name}
buttonType={{
text: item.on
? colors.accent
: item.name === 'Delete' || item.name === 'PermDelete'
? colors.errorText
: colors.pri
}}
onPress={item.onPress}
title={item.name}
icon={item.icon}
loading={loading?.name === item.name}
type={item.on ? 'shade' : 'gray'}
fontSize={SIZE.sm}
style={{
borderRadius: 0,
justifyContent: 'flex-start',
alignSelf: 'flex-start',
width: '100%'
}}
/>
))}
<View
style={{
paddingHorizontal: 12
}}
>
{failed ? (
<Notice
type="alert"
text={`File check failed with error: ${attachment.failed} Try reuploading the file to fix the issue.`}
size="small"
/>
) : null}
</View>
</ScrollView>
);
};
Actions.present = (attachment, set, context) => {
presentSheet({
context: context,
component: ref => <Actions fwdRef={ref} setAttachments={set} attachment={attachment} />
});
};
export default Actions;

View File

@@ -0,0 +1,144 @@
import React from 'react';
import { TouchableOpacity, View } from 'react-native';
import * as Progress from 'react-native-progress';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { useAttachmentStore } from '../../stores/stores';
import { useThemeStore } from '../../stores/theme';
import { formatBytes } from '../../utils';
import { db } from '../../utils/database';
import { useAttachmentProgress } from '../../utils/hooks/use-attachment-progress';
import { SIZE } from '../../utils/size';
import SheetProvider from '../sheet-provider';
import { IconButton } from '../ui/icon-button';
import Paragraph from '../ui/typography/paragraph';
import Actions from './actions';
function getFileExtension(filename) {
var ext = /^.+\.([^.]+)$/.exec(filename);
return ext == null ? '' : ext[1];
}
export const AttachmentItem = ({ attachment, encryption, setAttachments }) => {
const colors = useThemeStore(state => state.colors);
const [currentProgress, setCurrentProgress] = useAttachmentProgress(attachment, encryption);
const encryptionProgress = encryption
? useAttachmentStore(state => state.encryptionProgress)
: null;
const onPress = () => {
Actions.present(attachment, setAttachments, attachment.metadata.hash);
};
return (
<TouchableOpacity
activeOpacity={0.9}
onPress={onPress}
style={{
flexDirection: 'row',
marginVertical: 5,
justifyContent: 'space-between',
padding: 12,
paddingVertical: 6,
borderRadius: 5,
backgroundColor: colors.nav
}}
type="grayBg"
>
<SheetProvider context={attachment.metadata.hash} />
<View
style={{
flexShrink: 1,
flexDirection: 'row',
alignItems: 'center'
}}
>
<View
style={{
justifyContent: 'center',
alignItems: 'center',
marginLeft: -5
}}
>
<Icon name="file" size={SIZE.xxxl} color={colors.icon} />
<Paragraph
adjustsFontSizeToFit
size={6}
color={colors.light}
style={{
position: 'absolute'
}}
>
{getFileExtension(attachment.metadata.filename).toUpperCase()}
</Paragraph>
</View>
<View
style={{
flexShrink: 1,
marginLeft: 10
}}
>
<Paragraph
size={SIZE.sm - 1}
style={{
flexWrap: 'wrap',
marginBottom: 2.5
}}
numberOfLines={1}
lineBreakMode="middle"
color={colors.pri}
>
{attachment.metadata.filename}
</Paragraph>
<Paragraph color={colors.icon} size={SIZE.xs}>
{formatBytes(attachment.length)}{' '}
{currentProgress?.type ? '(' + currentProgress.type + 'ing - tap to cancel)' : ''}
</Paragraph>
</View>
</View>
{currentProgress || encryptionProgress || encryption ? (
<TouchableOpacity
activeOpacity={0.9}
onPress={() => {
if (encryption) return;
db.fs.cancel(attachment.metadata.hash);
setCurrentProgress(null);
}}
style={{
justifyContent: 'center',
marginLeft: 5,
marginTop: 5,
marginRight: -5
}}
>
<Progress.Circle
size={SIZE.xxl}
progress={
encryptionProgress
? encryptionProgress
: currentProgress?.value
? currentProgress?.value / 100
: 0
}
showsText
textStyle={{
fontSize: 10
}}
color={colors.accent}
formatText={progress => (progress * 100).toFixed(0)}
borderWidth={0}
thickness={2}
/>
</TouchableOpacity>
) : (
<>
{attachment.failed ? (
<IconButton onPress={onPress} name="alert-circle-outline" color={colors.errorText} />
) : null}
</>
)}
</TouchableOpacity>
);
};

View File

@@ -0,0 +1,176 @@
import React, { useEffect, useRef, useState } from 'react';
import { View } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { eSubscribeEvent, eUnSubscribeEvent } from '../../services/event-manager';
import { useThemeStore } from '../../stores/theme';
import { db } from '../../utils/database';
import { eCloseAttachmentDialog, eOpenAttachmentsDialog } from '../../utils/events';
import filesystem from '../../utils/filesystem';
import { SIZE } from '../../utils/size';
import DialogHeader from '../dialog/dialog-header';
import { Toast } from '../toast';
import Input from '../ui/input';
import Seperator from '../ui/seperator';
import SheetWrapper from '../ui/sheet';
import Paragraph from '../ui/typography/paragraph';
import { AttachmentItem } from './attachment-item';
export const AttachmentDialog = () => {
const colors = useThemeStore(state => state.colors);
const [visible, setVisible] = useState(false);
const [note, setNote] = useState(null);
const actionSheetRef = useRef();
const [attachments, setAttachments] = useState([]);
const attachmentSearchValue = useRef();
const searchTimer = useRef();
const [loading, setLoading] = useState(false);
useEffect(() => {
eSubscribeEvent(eOpenAttachmentsDialog, open);
eSubscribeEvent(eCloseAttachmentDialog, close);
return () => {
eUnSubscribeEvent(eOpenAttachmentsDialog, open);
eUnSubscribeEvent(eCloseAttachmentDialog, close);
};
}, [visible]);
const open = data => {
if (data?.id) {
setNote(data);
let _attachments = db.attachments.ofNote(data.id, 'all');
setAttachments(_attachments);
} else {
setAttachments([...db.attachments.all]);
}
setVisible(true);
};
useEffect(() => {
if (visible) {
actionSheetRef.current?.show();
}
}, [visible]);
const close = () => {
actionSheetRef.current?.hide();
setVisible(false);
};
const onChangeText = text => {
attachmentSearchValue.current = text;
console.log(attachmentSearchValue.current?.length);
if (!attachmentSearchValue.current || attachmentSearchValue.current === '') {
console.log('resetting all');
setAttachments([...db.attachments.all]);
}
console.log(attachments.length);
clearTimeout(searchTimer.current);
searchTimer.current = setTimeout(() => {
let results = db.lookup.attachments(db.attachments.all, attachmentSearchValue.current);
console.log('results', results.length, attachments.length);
if (results.length === 0) return;
setAttachments(results);
}, 300);
};
const renderItem = ({ item, index }) => (
<AttachmentItem setAttachments={setAttachments} attachment={item} />
);
return !visible ? null : (
<SheetWrapper
centered={false}
fwdRef={actionSheetRef}
onClose={async () => {
setVisible(false);
}}
>
<Toast context="local" />
<View
style={{
width: '100%',
alignSelf: 'center',
paddingHorizontal: 12
}}
>
<DialogHeader
title={note ? 'Attachments' : 'Manage attachments'}
paragraph="Tap on an attachment to view properties"
button={{
title: 'Check all',
type: 'grayAccent',
loading: loading,
onPress: async () => {
setLoading(true);
for (let attachment of attachments) {
let result = await filesystem.checkAttachment(attachment.metadata.hash);
if (result.failed) {
db.attachments.markAsFailed(attachment.metadata.hash, result.failed);
} else {
db.attachments.markAsFailed(attachment.id, null);
}
setAttachments([...db.attachments.all]);
}
setLoading(false);
}
}}
/>
<Seperator />
{!note ? (
<Input
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current);
}}
/>
) : null}
<FlatList
nestedScrollEnabled
overScrollMode="never"
scrollToOverflowEnabled={false}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
onMomentumScrollEnd={() => {
actionSheetRef.current?.handleChildScrollEnd();
}}
ListEmptyComponent={
<View
style={{
height: 150,
justifyContent: 'center',
alignItems: 'center'
}}
>
<Icon name="attachment" size={60} color={colors.icon} />
<Paragraph>{note ? `No attachments on this note` : `No attachments`}</Paragraph>
</View>
}
ListFooterComponent={
<View
style={{
height: 350
}}
/>
}
data={attachments}
keyExtractor={item => item.id}
renderItem={renderItem}
/>
<Paragraph
color={colors.icon}
size={SIZE.xs}
style={{
textAlign: 'center',
marginTop: 10
}}
>
<Icon name="shield-key-outline" size={SIZE.xs} color={colors.icon} />
{' '}All attachments are end-to-end encrypted.
</Paragraph>
</View>
</SheetWrapper>
);
};

View File

@@ -0,0 +1,2 @@
export const SVG = color =>
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="1920" height="1080" preserveAspectRatio="none" viewBox="0 0 1920 1080"><g fill="none"><path d="M684.78-215.3C516.78-49.87 619.54 659.12 349.29 666.13 79.03 673.14-116.04 204.76-321.7 180.13" stroke="${color}" stroke-width="2"></path><path d="M1337.71-20.29C1105.2 122.03 1140.53 887.64 809.58 933.71 478.63 979.78 545.52 798.71 281.45 798.71 17.39 798.71-110.41 932.65-246.68 933.71" stroke="${color}" stroke-width="2"></path><path d="M1631.89-52.3C1379.35-21.93 1283.05 511.66 808.42 545.28 333.79 578.9 209.15 894.14-15.05 901.68" stroke="${color}" stroke-width="2"></path><path d="M1523.21-71.7C1244.4-46.38 1056.2 532.95 581.02 533.49 105.84 534.03-109.4 182.98-361.17 177.09" stroke="${color}" stroke-width="2"></path><path d="M1333.35-39.1C1041.86 57 974.42 893.52 550.36 906.48 126.3 919.44-23.8 619.56-232.63 614.88" stroke="${color}" stroke-width="2"></path></g><defs><mask id="SvgjsMask1032"><rect width="1920" height="1080" fill="#ffffff"></rect></mask></defs></svg>`;

View File

@@ -0,0 +1,130 @@
import React, { useRef, useState } from 'react';
import { View } from 'react-native';
import { useThemeStore } from '../../stores/theme';
import { useUserStore } from '../../stores/stores';
import { eSendEvent, presentSheet, ToastEvent } from '../../services/event-manager';
import { db } from '../../utils/database';
import { eCloseProgressDialog } from '../../utils/events';
import { Button } from '../ui/button';
import DialogHeader from '../dialog/dialog-header';
import Input from '../ui/input';
import { Notice } from '../ui/notice';
import Seperator from '../ui/seperator';
export const ChangePassword = () => {
const colors = useThemeStore(state => state.colors);
const passwordInputRef = useRef();
const password = useRef();
const oldPasswordInputRef = useRef();
const oldPassword = useRef();
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const user = useUserStore(state => state.user);
const changePassword = async () => {
if (!user.isEmailConfirmed) {
ToastEvent.show({
heading: 'Email not confirmed',
message: 'Please confirm your email to change account password',
type: 'error',
context: 'local'
});
return;
}
if (error || !oldPassword.current || !password.current) {
ToastEvent.show({
heading: 'All fields required',
message: 'Fill all the fields and try again.',
type: 'error',
context: 'local'
});
return;
}
setLoading(true);
try {
await db.user.clearSessions();
await db.user.changePassword(oldPassword.current, password.current);
ToastEvent.show({
heading: `Account password updated`,
type: 'success',
context: 'global'
});
setLoading(false);
eSendEvent(eCloseProgressDialog);
} catch (e) {
setLoading(false);
ToastEvent.show({
heading: 'Failed to change password',
message: e.message,
type: 'error',
context: 'local'
});
}
setLoading(false);
};
return (
<View
style={{
width: '100%',
padding: 12
}}
>
<DialogHeader title="Change password" paragraph="Enter your old and new passwords" />
<Seperator />
<Input
fwdRef={oldPasswordInputRef}
onChangeText={value => {
oldPassword.current = value;
}}
returnKeyLabel="Next"
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder="Old Password"
/>
<Input
fwdRef={passwordInputRef}
onChangeText={value => {
password.current = value;
}}
onErrorCheck={e => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
secureTextEntry
validationType="password"
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder="New password"
/>
<Notice
text="Changing password is a non-undoable process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection."
type="alert"
/>
<Button
style={{
marginTop: 10,
width: '100%'
}}
loading={loading}
onPress={changePassword}
type="accent"
title={loading ? null : 'I understand, change my password'}
/>
</View>
);
};
ChangePassword.present = () => {
presentSheet({
component: <ChangePassword />
});
};

View File

@@ -0,0 +1,157 @@
import React, { useRef, useState } from 'react';
import { View } from 'react-native';
import ActionSheet from 'react-native-actions-sheet';
import { DDS } from '../../services/device-detection';
import { ToastEvent } from '../../services/event-manager';
import SettingsService from '../../services/settings';
import { useThemeStore } from '../../stores/theme';
import { db } from '../../utils/database';
import DialogHeader from '../dialog/dialog-header';
import { Button } from '../ui/button';
import { IconButton } from '../ui/icon-button';
import Input from '../ui/input';
import Seperator from '../ui/seperator';
import Heading from '../ui/typography/heading';
import Paragraph from '../ui/typography/paragraph';
export const ForgotPassword = () => {
const colors = useThemeStore(state => state.colors);
const email = useRef();
const emailInputRef = useRef();
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const [sent, setSent] = useState(false);
const sendRecoveryEmail = async () => {
if (!email.current || error) {
ToastEvent.show({
heading: 'Account email is required.',
type: 'error',
context: 'local'
});
return;
}
setLoading(true);
try {
let lastRecoveryEmailTime = SettingsService.get().lastRecoveryEmailTime;
if (lastRecoveryEmailTime && Date.now() - JSON.parse(lastRecoveryEmailTime) < 60000 * 3) {
throw new Error('Please wait before requesting another email');
}
await db.user.recoverAccount(email.current.toLowerCase());
SettingsService.set({
lastRecoveryEmailTime: Date.now()
});
ToastEvent.show({
heading: `Check your email to reset password`,
message: `Recovery email has been sent to ${email.current.toLowerCase()}`,
type: 'success',
context: 'local',
duration: 7000
});
setLoading(false);
setSent(true);
} catch (e) {
setLoading(false);
ToastEvent.show({
heading: 'Recovery email not sent',
message: e.message,
type: 'error',
context: 'local'
});
}
};
return (
<>
<ActionSheet
onBeforeShow={data => (email.current = data)}
onClose={() => {
setSent(false);
setLoading(false);
}}
keyboardShouldPersistTaps="always"
onOpen={() => {
emailInputRef.current?.setNativeProps({
text: email.current
});
}}
gestureEnabled
id="forgotpassword_sheet"
>
{sent ? (
<View
style={{
padding: 12,
justifyContent: 'center',
alignItems: 'center',
paddingBottom: 50
}}
>
<IconButton
customStyle={{
width: null,
height: null
}}
color={colors.accent}
name="email"
size={50}
/>
<Heading>Recovery email sent!</Heading>
<Paragraph
style={{
textAlign: 'center'
}}
>
Please follow the link in the email to recover your account.
</Paragraph>
</View>
) : (
<View
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.bg,
zIndex: 10,
width: '100%',
padding: 12
}}
>
<DialogHeader
title="Account recovery"
paragraph="We will send you an email with steps on how to reset your password."
/>
<Seperator />
<Input
fwdRef={emailInputRef}
onChangeText={value => {
email.current = value;
}}
defaultValue={email.current}
onErrorCheck={e => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage="Email is invalid"
placeholder="Email"
onSubmit={() => {}}
/>
<Button
style={{
marginTop: 10,
width: '100%'
}}
loading={loading}
onPress={sendRecoveryEmail}
type="accent"
title={loading ? null : 'Next'}
/>
</View>
)}
</ActionSheet>
</>
);
};

View File

@@ -0,0 +1,73 @@
import React, { useEffect, useRef, useState } from 'react';
import { useThemeStore } from '../../stores/theme';
import { eSubscribeEvent, eUnSubscribeEvent } from '../../services/event-manager';
import { eCloseLoginDialog, eOpenLoginDialog } from '../../utils/events';
import { sleep } from '../../utils/time';
import BaseDialog from '../dialog/base-dialog';
import { Toast } from '../toast';
import { Login } from './login';
import { Signup } from './signup';
export const AuthMode = {
login: 0,
signup: 1,
welcomeSignup: 2,
trialSignup: 3
};
const Auth = () => {
const colors = useThemeStore(state => state.colors);
const [visible, setVisible] = useState(false);
const [currentAuthMode, setCurrentAuthMode] = useState(AuthMode.login);
const actionSheetRef = useRef();
useEffect(() => {
eSubscribeEvent(eOpenLoginDialog, open);
eSubscribeEvent(eCloseLoginDialog, close);
return () => {
eUnSubscribeEvent(eOpenLoginDialog, open);
eUnSubscribeEvent(eCloseLoginDialog, close);
};
}, []);
async function open(mode) {
setCurrentAuthMode(mode ? mode : AuthMode.login);
setVisible(true);
await sleep(10);
actionSheetRef.current?.show();
}
const close = () => {
actionSheetRef.current?.hide();
setCurrentAuthMode(AuthMode.login);
setVisible(false);
};
return !visible ? null : (
<BaseDialog
overlayOpacity={0}
statusBarTranslucent={false}
onRequestClose={currentAuthMode !== AuthMode.welcomeSignup && close}
visible={true}
onClose={close}
useSafeArea={false}
bounce={false}
background={colors.bg}
transparent={false}
>
<Toast context="local" />
{currentAuthMode !== AuthMode.login ? (
<Signup
changeMode={mode => setCurrentAuthMode(mode)}
trial={AuthMode.trialSignup === currentAuthMode}
welcome={currentAuthMode === AuthMode.welcomeSignup}
/>
) : (
<Login changeMode={mode => setCurrentAuthMode(mode)} />
)}
</BaseDialog>
);
};
export default Auth;

View File

@@ -0,0 +1,283 @@
import React, { useEffect, useRef, useState } from 'react';
import { Platform, View } from 'react-native';
import { SheetManager } from 'react-native-actions-sheet';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { DDS } from '../../services/device-detection';
import { eSendEvent, presentSheet, ToastEvent } from '../../services/event-manager';
import { clearMessage } from '../../services/message';
import PremiumService from '../../services/premium';
import SettingsService from '../../services/settings';
import { useUserStore } from '../../stores/stores';
import { useThemeStore } from '../../stores/theme';
import { db } from '../../utils/database';
import { eCloseLoginDialog } from '../../utils/events';
import { SIZE } from '../../utils/size';
import { sleep } from '../../utils/time';
import BaseDialog from '../dialog/base-dialog';
import SheetProvider from '../sheet-provider';
import { Button } from '../ui/button';
import { IconButton } from '../ui/icon-button';
import Input from '../ui/input';
import { SvgView } from '../ui/svg';
import { BouncingView } from '../ui/transitions/bouncing-view';
import Heading from '../ui/typography/heading';
import Paragraph from '../ui/typography/paragraph';
import { SVG } from './background';
import { ForgotPassword } from './forgot-password';
import TwoFactorVerification from './two-factor';
export const Login = ({ changeMode }) => {
const colors = useThemeStore(state => state.colors);
const email = useRef();
const emailInputRef = useRef();
const passwordInputRef = useRef();
const password = useRef();
const [focused, setFocused] = useState(false);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const insets = useSafeAreaInsets();
const setUser = useUserStore(state => state.setUser);
const validateInfo = () => {
if (!password.current || !email.current) {
ToastEvent.show({
heading: 'All fields required',
message: 'Fill all the fields and try again',
type: 'error',
context: 'local'
});
return false;
}
return true;
};
useEffect(() => {
async () => {
await sleep(500);
emailInputRef.current?.focus();
setFocused(true);
};
}, []);
const login = async (mfa, callback) => {
if (!validateInfo() || error) return;
setLoading(true);
let user;
try {
if (mfa) {
console.log('mfa details', mfa);
await db.user.mfaLogin(email.current.toLowerCase(), password.current, mfa);
} else {
await db.user.login(email.current.toLowerCase(), password.current);
}
callback && callback(true);
user = await db.user.getUser();
if (!user) throw new Error('Email or password incorrect!');
PremiumService.setPremiumStatus();
setUser(user);
clearMessage();
ToastEvent.show({
heading: 'Login successful',
message: `Logged in as ${user.email}`,
type: 'success',
context: 'global'
});
eSendEvent(eCloseLoginDialog);
await SettingsService.set({
sessionExpired: false,
userEmailConfirmed: user.isEmailConfirmed
});
eSendEvent('userLoggedIn', true);
await sleep(500);
presentSheet({
title: 'Syncing your data',
paragraph: 'Please wait while we sync all your data.',
progress: true
});
} catch (e) {
callback && callback(false);
console.log('Login error', e.message, e.data);
if (e.message === 'Multifactor authentication required.') {
TwoFactorVerification.present(async mfa => {
if (mfa) {
console.log(mfa);
await login(mfa);
} else {
setLoading(false);
}
}, e.data);
} else {
console.log(e.stack);
setLoading(false);
ToastEvent.show({
heading: user ? 'Failed to sync' : 'Login failed',
message: e.message,
type: 'error',
context: 'local'
});
}
}
};
return (
<>
<IconButton
name="arrow-left"
onPress={() => {
eSendEvent(eCloseLoginDialog);
}}
color={colors.pri}
customStyle={{
position: 'absolute',
zIndex: 999,
left: 12,
top: Platform.OS === 'ios' ? 12 + insets.top : 12
}}
/>
<ForgotPassword />
<SheetProvider context="two_factor_verify" />
{loading ? <BaseDialog transparent={true} visible={true} animation="fade" /> : null}
<View
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.bg,
zIndex: 10,
width: '100%',
minHeight: '100%'
}}
>
<View
style={{
height: 250,
overflow: 'hidden'
}}
>
<BouncingView initialScale={1.05} duration={5000}>
<SvgView src={SVG(colors.night ? colors.icon : 'black')} height={700} />
</BouncingView>
</View>
<BouncingView initialScale={0.98} duration={3000}>
<View
style={{
width: '100%',
justifyContent: 'center',
alignSelf: 'center',
paddingHorizontal: 12,
marginBottom: 30,
marginTop: 15
}}
>
<Heading
style={{
textAlign: 'center'
}}
size={30}
color={colors.heading}
>
Welcome back!
</Heading>
<Paragraph
style={{
textDecorationLine: 'underline',
textAlign: 'center',
marginTop: 5
}}
onPress={() => {
changeMode(1);
}}
size={SIZE.md}
>
Don't have an account? Sign up
</Paragraph>
</View>
<View
style={{
width: focused ? '100%' : '99.9%',
padding: 12,
backgroundColor: colors.bg,
flexGrow: 1
}}
>
<Input
fwdRef={emailInputRef}
onChangeText={value => {
email.current = value;
}}
onErrorCheck={e => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage="Email is invalid"
placeholder="Email"
onSubmit={() => {
passwordInputRef.current?.focus();
}}
/>
<Input
fwdRef={passwordInputRef}
onChangeText={value => {
password.current = value;
}}
returnKeyLabel="Done"
returnKeyType="done"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder="Password"
marginBottom={0}
onSubmit={login}
/>
<Button
title="Forgot your password?"
style={{
alignSelf: 'flex-end',
height: 30,
paddingHorizontal: 0
}}
onPress={() => {
SheetManager.show('forgotpassword_sheet', email.current);
}}
textStyle={{
textDecorationLine: 'underline'
}}
fontSize={SIZE.xs}
type="gray"
/>
<View
style={{
// position: 'absolute',
marginTop: 50,
alignSelf: 'center'
}}
>
<Button
style={{
marginTop: 10,
width: 250,
borderRadius: 100
}}
loading={loading}
onPress={login}
// width="100%"
type="accent"
title={loading ? null : 'Login to your account'}
/>
</View>
</View>
</BouncingView>
</View>
</>
);
};

View File

@@ -0,0 +1,260 @@
import React, { useEffect, useRef, useState } from 'react';
import { Modal, View } from 'react-native';
import { useThemeStore } from '../../stores/theme';
import { useUserStore } from '../../stores/stores';
import BiometricService from '../../services/biometrics';
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
presentSheet,
ToastEvent
} from '../../services/event-manager';
import { clearMessage } from '../../services/message';
import PremiumService from '../../services/premium';
import Sync from '../../services/sync';
import { db } from '../../utils/database';
import { MMKV } from '../../utils/database/mmkv';
import { SIZE } from '../../utils/size';
import { sleep } from '../../utils/time';
import { IconButton } from '../ui/icon-button';
import { Button } from '../ui/button';
import { Dialog } from '../dialog';
import { presentDialog } from '../dialog/functions';
import Input from '../ui/input';
import { Toast } from '../toast';
import Heading from '../ui/typography/heading';
import Paragraph from '../ui/typography/paragraph';
import SettingsService from '../../services/settings';
import TwoFactorVerification from './two-factor';
import SheetProvider from '../sheet-provider';
function getEmail(email) {
if (!email) return null;
return email.replace(/(.{2})(.*)(?=@)/, function (gp1, gp2, gp3) {
for (let i = 0; i < gp3.length; i++) {
gp2 += '*';
}
return gp2;
});
}
export const SessionExpired = () => {
const colors = useThemeStore(state => state.colors);
const email = useRef();
const emailInputRef = useRef();
const passwordInputRef = useRef();
const password = useRef();
const [visible, setVisible] = useState(false);
const [focused, setFocused] = useState(false);
const setUser = useUserStore(state => state.setUser);
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const logout = async () => {
try {
await db.user.logout();
await BiometricService.resetCredentials();
await SettingsService.set({
introCompleted: true
});
setVisible(false);
} catch (e) {
ToastEvent.show({
heading: e.message,
type: 'error',
context: 'local'
});
}
};
useEffect(() => {
eSubscribeEvent('session_expired', open);
return () => {
eUnSubscribeEvent('session_expired', open);
setFocused(false);
};
}, [visible]);
const open = async () => {
try {
console.log('REQUESTING NEW TOKEN');
let res = await db.user.tokenManager.getToken();
if (!res) throw new Error('no token found');
if (db.user.tokenManager._isTokenExpired(res)) throw new Error('token expired');
if (!(await Sync.run())) throw new Error('e');
await SettingsService.set({
sessionExpired: false
});
setVisible(false);
} catch (e) {
console.log(e);
let user = await db.user.getUser();
if (!user) return;
email.current = user.email;
setVisible(true);
}
};
const login = async (mfa, callback) => {
if (error) return;
setLoading(true);
let user;
try {
if (mfa) {
await db.user.mfaLogin(email.current.toLowerCase(), password.current, mfa);
} else {
await db.user.login(email.current.toLowerCase(), password.current);
}
callback && callback(true);
user = await db.user.getUser();
if (!user) throw new Error('Email or password incorrect!');
PremiumService.setPremiumStatus();
setUser(user);
clearMessage();
ToastEvent.show({
heading: 'Login successful',
message: `Logged in as ${user.email}`,
type: 'success',
context: 'global'
});
await SettingsService.set({
sessionExpired: false,
userEmailConfirmed: user.isEmailConfirmed
});
eSendEvent('userLoggedIn', true);
await sleep(500);
presentSheet({
title: 'Syncing your data',
paragraph: 'Please wait while we sync all your data.',
progress: true
});
setLoading(false);
} catch (e) {
callback && callback(false);
if (e.message === 'Multifactor authentication required.') {
TwoFactorVerification.present(async mfa => {
if (mfa) {
console.log(mfa);
await login(mfa);
} else {
setLoading(false);
}
}, e.data);
} else {
console.log(e.stack);
setLoading(false);
ToastEvent.show({
heading: user ? 'Failed to sync' : 'Login failed',
message: e.message,
type: 'error',
context: 'local'
});
}
}
};
return (
visible && (
<Modal
onShow={async () => {
await sleep(300);
passwordInputRef.current?.focus();
setFocused(true);
}}
visible={true}
>
<SheetProvider context="two_factor_verify" />
<View
style={{
width: focused ? '100%' : '99.9%',
padding: 12,
justifyContent: 'center',
flex: 1
}}
>
<View
style={{
justifyContent: 'center',
alignItems: 'center',
width: '100%',
marginBottom: 20,
borderRadius: 10,
paddingVertical: 20
}}
>
<IconButton
customStyle={{
width: 60,
height: 60
}}
name="alert"
color={colors.errorText}
size={50}
/>
<Heading size={SIZE.xxxl} color={colors.heading}>
Session expired
</Heading>
<Paragraph
style={{
textAlign: 'center'
}}
>
Your session on this device has expired. Please enter password for{' '}
{getEmail(email.current)} to continue.
</Paragraph>
</View>
<Input
fwdRef={passwordInputRef}
onChangeText={value => {
password.current = value;
}}
returnKeyLabel="Next"
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder="Password"
onSubmit={login}
/>
<Button
style={{
marginTop: 10,
width: '100%'
}}
loading={loading}
onPress={login}
type="accent"
title={loading ? null : 'Login'}
/>
<Button
style={{
marginTop: 10,
width: '100%'
}}
onPress={() => {
presentDialog({
context: 'session_expiry',
title: 'Logout',
paragraph:
'Are you sure you want to logout from this device? Any unsynced changes will be lost.',
positiveText: 'Logout',
positiveType: 'errorShade',
positivePress: logout
});
}}
type="errorShade"
title={loading ? null : 'Logout from this device'}
/>
</View>
<Toast context="local" />
<Dialog context="session_expiry" />
</Modal>
)
);
};

View File

@@ -0,0 +1,293 @@
import React, { useRef, useState } from 'react';
import { Dimensions, Platform, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useThemeStore } from '../../stores/theme';
import { useUserStore } from '../../stores/stores';
import { DDS } from '../../services/device-detection';
import { eSendEvent, ToastEvent } from '../../services/event-manager';
import { clearMessage, setEmailVerifyMessage } from '../../services/message';
import PremiumService from '../../services/premium';
import { db } from '../../utils/database';
import { eCloseLoginDialog } from '../../utils/events';
import { openLinkInBrowser } from '../../utils/functions';
import { SIZE } from '../../utils/size';
import { sleep } from '../../utils/time';
import umami from '../../utils/analytics';
import { IconButton } from '../ui/icon-button';
import { Button } from '../ui/button';
import BaseDialog from '../dialog/base-dialog';
import Input from '../ui/input';
import { SvgView } from '../ui/svg';
import { BouncingView } from '../ui/transitions/bouncing-view';
import Heading from '../ui/typography/heading';
import Paragraph from '../ui/typography/paragraph';
import { SVG } from './background';
export const Signup = ({ changeMode, welcome, trial }) => {
const colors = useThemeStore(state => state.colors);
const email = useRef();
const emailInputRef = useRef();
const passwordInputRef = useRef();
const password = useRef();
const confirmPasswordInputRef = useRef();
const confirmPassword = useRef();
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const insets = useSafeAreaInsets();
const setUser = useUserStore(state => state.setUser);
const setLastSynced = useUserStore(state => state.setLastSynced);
const validateInfo = () => {
if (!password.current || !email.current || !confirmPassword.current) {
ToastEvent.show({
heading: 'All fields required',
message: 'Fill all the fields and try again',
type: 'error',
context: 'local'
});
return false;
}
return true;
};
const signup = async () => {
if (!validateInfo() || error) return;
setLoading(true);
try {
await db.user.signup(email.current.toLowerCase(), password.current);
let user = await db.user.getUser();
setUser(user);
setLastSynced(await db.lastSynced());
clearMessage();
setEmailVerifyMessage();
eSendEvent(eCloseLoginDialog);
umami.pageView('/account-created', '/welcome/signup');
await sleep(300);
if (trial) {
PremiumService.sheet(null, null, true);
} else {
PremiumService.showVerifyEmailDialog();
}
} catch (e) {
setLoading(false);
ToastEvent.show({
heading: 'Signup failed',
message: e.message,
type: 'error',
context: 'local'
});
}
};
return (
<>
{!welcome && (
<IconButton
name="arrow-left"
onPress={() => {
eSendEvent(eCloseLoginDialog);
}}
color={colors.pri}
customStyle={{
position: 'absolute',
zIndex: 999,
left: 12,
top: Platform.OS === 'ios' ? 12 + insets.top : 12
}}
/>
)}
{loading ? <BaseDialog transparent={true} visible={true} animation="fade" /> : null}
<View
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.bg,
zIndex: 10,
width: '100%',
minHeight: '100%'
}}
>
<View
style={{
height: 250,
overflow: 'hidden'
}}
>
<BouncingView initialScale={1.05} duration={5000}>
<SvgView src={SVG(colors.night ? colors.icon : 'black')} height={700} />
</BouncingView>
</View>
<BouncingView initialScale={0.98} duration={5000}>
<View
style={{
width: '100%',
justifyContent: 'center',
alignSelf: 'center',
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={() => {
changeMode(0);
}}
size={SIZE.md}
>
Already have an account? Log in
</Paragraph>
</View>
<View
style={{
width: '100%',
padding: 12,
backgroundColor: colors.bg,
flexGrow: 1
}}
>
<Input
fwdRef={emailInputRef}
onChangeText={value => {
email.current = value;
}}
onErrorCheck={e => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
validationType="email"
autoCorrect={false}
autoCapitalize="none"
errorMessage="Email is invalid"
placeholder="Email"
onSubmit={() => {
passwordInputRef.current?.focus();
}}
/>
<Input
fwdRef={passwordInputRef}
onChangeText={value => {
password.current = value;
}}
onErrorCheck={e => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
validationType="password"
autoCorrect={false}
placeholder="Password"
onSubmit={() => {
confirmPasswordInputRef.current?.focus();
}}
/>
<Input
fwdRef={confirmPasswordInputRef}
onChangeText={value => {
confirmPassword.current = value;
}}
onErrorCheck={e => setError(e)}
returnKeyLabel="Signup"
returnKeyType="done"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
validationType="confirmPassword"
customValidator={() => password.current}
placeholder="Confirm password"
marginBottom={5}
onSubmit={signup}
/>
<Paragraph size={SIZE.xs} color={colors.icon}>
By signing up, you agree to our{' '}
<Paragraph
size={SIZE.xs}
onPress={() => {
openLinkInBrowser('https://notesnook.com/tos', colors)
.catch(e => {})
.then(r => {});
}}
style={{
textDecorationLine: 'underline'
}}
color={colors.accent}
>
terms of service{' '}
</Paragraph>
and{' '}
<Paragraph
size={SIZE.xs}
onPress={() => {
openLinkInBrowser('https://notesnook.com/privacy', colors)
.catch(e => {})
.then(r => {});
}}
style={{
textDecorationLine: 'underline'
}}
color={colors.accent}
>
privacy policy.
</Paragraph>
</Paragraph>
<View
style={{
marginTop: 50,
alignSelf: 'center'
}}
>
<Button
style={{
marginTop: 10,
width: 250,
borderRadius: 100
}}
loading={loading}
onPress={signup}
type="accent"
title={loading ? null : 'Agree and continue'}
/>
{loading || !welcome ? null : (
<Button
style={{
marginTop: 10,
width: 250,
borderRadius: 100
}}
onPress={() => {
eSendEvent(eCloseLoginDialog);
}}
type="grayBg"
title="Skip for now"
/>
)}
</View>
</View>
</BouncingView>
</View>
</>
);
};

View File

@@ -0,0 +1,284 @@
import React, { useEffect, useRef, useState } from 'react';
import { View } from 'react-native';
import { eSendEvent, presentSheet } from '../../services/event-manager';
import { useThemeStore } from '../../stores/theme';
import { eCloseProgressDialog } from '../../utils/events';
import useTimer from '../../utils/hooks/use-timer';
import { SIZE } from '../../utils/size';
import { Button } from '../ui/button';
import { IconButton } from '../ui/icon-button';
import Input from '../ui/input';
import { PressableButton } from '../ui/pressable';
import Seperator from '../ui/seperator';
import Heading from '../ui/typography/heading';
import Paragraph from '../ui/typography/paragraph';
import { db } from '../../utils/database/index';
import { ToastEvent } from '../../services/event-manager';
const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
const colors = useThemeStore(state => state.colors);
const code = useRef();
const [currentMethod, setCurrentMethod] = useState({
method: mfaInfo?.primaryMethod,
isPrimary: true
});
const { seconds, start } = useTimer(currentMethod.method);
const [loading, setLoading] = useState(false);
const inputRef = useRef();
const [sending, setSending] = useState(false);
const codeHelpText = {
app: 'Enter the 6 digit code from your authenticator app to continue logging in',
sms: 'Enter the 6 digit code sent to your phone number to continue logging in',
email: 'Enter the 6 digit code sent to your email to continue logging in',
recoveryCode: 'Enter the 8 digit recovery code to continue logging in'
};
const secondaryMethodsText = {
app: "I don't have access to authenticator app",
sms: "I don't have access to my phone",
email: "I don't have access to email",
recoveryCode: "I don't have recovery codes"
};
const onNext = async () => {
const length = currentMethod.method === 'recoveryCode' ? 8 : 6;
if (!code.current || code.current.length !== length) return;
console.log(currentMethod.method, code.current);
setLoading(true);
inputRef.current?.blur();
await onMfaLogin(
{
method: currentMethod.method,
code: code.current
},
result => {
console.log('result recieved');
if (result) {
eSendEvent(eCloseProgressDialog, 'two_factor_verify');
}
setLoading(false);
}
);
setLoading(false);
};
const onRequestSecondaryMethod = () => {
setCurrentMethod({
method: null,
isPrimary: false
});
};
const methods = [
{
id: 'sms',
title: 'Send code via SMS',
icon: 'message-plus-outline'
},
{
id: 'email',
title: 'Send code via email',
icon: 'email-outline'
},
{
id: 'app',
title: 'Enter code from authenticator app',
icon: 'cellphone-key'
},
{
id: 'recoveryCode',
title: 'I have a recovery code',
icon: 'key'
}
];
const getMethods = () => {
return methods.filter(
m =>
m.id === mfaInfo?.primaryMethod ||
m.id === mfaInfo?.secondaryMethod ||
m.id === 'recoveryCode'
);
};
useEffect(() => {
if (currentMethod.method === 'sms' || currentMethod.method === 'email') {
onSendCode();
}
}, [currentMethod.method]);
const onSendCode = async () => {
if (seconds || sending) return;
// TODO
setSending(true);
try {
console.log('sending code', currentMethod.method, mfaInfo.token);
await db.mfa.sendCode(currentMethod.method, mfaInfo.token);
start(60);
setSending(false);
} catch (e) {
setSending(false);
ToastEvent.error(e, 'Error sending 2FA Code', 'local');
}
};
return (
<View>
<View
style={{
alignItems: 'center',
paddingHorizontal: currentMethod.method ? 12 : 0
}}
>
<IconButton
customStyle={{
width: 70,
height: 70
}}
size={50}
name="key"
color={colors.accent}
/>
<Heading
style={{
textAlign: 'center'
}}
>
{currentMethod.method
? 'Two factor authentication'
: 'Select methods for two-factor authentication'}
</Heading>
<Paragraph
style={{
width: '80%',
textAlign: 'center'
}}
>
{codeHelpText[currentMethod.method] || `Select how you would like to recieve the code`}
</Paragraph>
<Seperator />
{currentMethod.method === 'sms' || currentMethod.method === 'email' ? (
<Button
onPress={onSendCode}
type={seconds ? 'gray' : 'transparent'}
title={sending ? '' : `${seconds ? `Resend code in (${seconds})` : 'Send code'}`}
loading={sending}
height={30}
/>
) : null}
<Seperator />
{currentMethod.method ? (
<>
<Input
placeholder={currentMethod.method === 'recoveryCode' ? 'xxxxxxxx' : 'xxxxxx'}
maxLength={currentMethod.method === 'recoveryCode' ? 8 : 6}
fwdRef={inputRef}
textAlign="center"
onChangeText={value => {
code.current = value;
onNext();
}}
//@ts-ignore
inputStyle={{
fontSize: SIZE.lg,
height: 60,
textAlign: 'center',
letterSpacing: 10,
width: null
}}
keyboardType={currentMethod.method === 'recoveryCode' ? 'default' : 'numeric'}
containerStyle={{
height: 60,
borderWidth: 0,
//@ts-ignore
width: null,
minWidth: '50%'
}}
/>
<Seperator />
<Button
title={loading ? null : 'Next'}
type="accent"
width={250}
loading={loading}
onPress={onNext}
style={{
borderRadius: 100,
marginBottom: 10
}}
/>
<Button
title={secondaryMethodsText[currentMethod.method]}
type="gray"
onPress={onRequestSecondaryMethod}
height={30}
/>
</>
) : (
<>
{getMethods().map(item => (
<PressableButton
key={item.title}
onPress={() => {
setCurrentMethod({
method: item.id,
isPrimary: false
});
}}
customStyle={{
paddingHorizontal: 12,
paddingVertical: 12,
marginTop: 0,
flexDirection: 'row',
borderRadius: 0,
alignItems: 'center',
width: '100%',
justifyContent: 'flex-start'
}}
>
<IconButton
type="grayAccent"
customStyle={{
width: 40,
height: 40,
marginRight: 10
}}
size={15}
color={colors.accent}
name={item.icon}
/>
<View
style={{
flexShrink: 1
}}
>
<Paragraph size={SIZE.md}>{item.title}</Paragraph>
</View>
</PressableButton>
))}
</>
)}
</View>
</View>
);
};
TwoFactorVerification.present = (onMfaLogin, data) => {
presentSheet({
component: <TwoFactorVerification onMfaLogin={onMfaLogin} mfaInfo={data} />,
context: 'two_factor_verify',
onClose: () => {
onMfaLogin();
},
disableClosing: true
});
};
export default TwoFactorVerification;

View File

@@ -1,11 +1,10 @@
import React from 'react';
import { View } from 'react-native';
import { useTracked } from '../../provider';
import { useSelectionStore } from '../../provider/stores';
import { useThemeStore } from '../../stores/theme';
import { useSelectionStore } from '../../stores/stores';
export const ContainerTopSection = ({ children }) => {
const [state] = useTracked();
const { colors } = state;
export const ContainerHeader = ({ children }) => {
const colors = useThemeStore(state => state.colors);
const selectionMode = useSelectionStore(state => state.selectionMode);
return !selectionMode ? (

View File

@@ -1,18 +1,23 @@
import React, { useEffect } from 'react';
import { Keyboard, Platform, View } from 'react-native';
import Animated, { Easing, sub } from 'react-native-reanimated';
import Animated, { Easing } from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { notesnook } from '../../../e2e/test.ids';
import { useSettingStore } from '../../provider/stores';
import { useSelectionStore, useSettingStore } from '../../stores/stores';
import { editing, getElevation, showTooltip, TOOLTIP_POSITIONS } from '../../utils';
import { normalize, SIZE } from '../../utils/SizeUtils';
import { PressableButton } from '../PressableButton';
import { normalize, SIZE } from '../../utils/size';
import { PressableButton } from '../ui/pressable';
const translateY = new Animated.Value(0);
export const ContainerBottomButton = ({ title, onPress, color = 'accent', shouldShow = false }) => {
export const FloatingButton = ({ title, onPress, color = 'accent', shouldShow = false }) => {
const insets = useSafeAreaInsets();
const deviceMode = useSettingStore(state => state.deviceMode);
const selectionMode = useSelectionStore(state => state.selectionMode);
useEffect(() => {
animate(selectionMode ? 150 : 0);
}, [selectionMode]);
function animate(translate) {
Animated.timing(translateY, {

View File

@@ -1,10 +1,9 @@
import React from 'react';
import { KeyboardAvoidingView, Platform, SafeAreaView } from 'react-native';
import { useTracked } from '../../provider';
import useIsFloatingKeyboard from '../../utils/use-is-floating-keyboard';
import { useThemeStore } from '../../stores/theme';
import useIsFloatingKeyboard from '../../utils/hooks/use-is-floating-keyboard';
export const Container = ({ children }) => {
const [state] = useTracked();
const { colors } = state;
const colors = useThemeStore(state => state.colors);
const floating = useIsFloatingKeyboard();
return (
<KeyboardAvoidingView

View File

@@ -1,16 +1,16 @@
import React, { useEffect } from 'react';
import {
Keyboard,
KeyboardAvoidingView,
Modal,
Platform,
SafeAreaView,
StyleSheet,
TouchableOpacity
TouchableOpacity,
View
} from 'react-native';
import { useSettingStore } from '../../provider/stores';
import useIsFloatingKeyboard from '../../utils/use-is-floating-keyboard';
import { BouncingView } from '../Transitions/bouncing-view';
import { useSettingStore } from '../../stores/stores';
import useIsFloatingKeyboard from '../../utils/hooks/use-is-floating-keyboard';
import { BouncingView } from '../ui/transitions/bouncing-view';
const BaseDialog = ({
visible,
@@ -26,7 +26,8 @@ const BaseDialog = ({
background = null,
animated = true,
bounce = true,
closeOnTouch = true
closeOnTouch = true,
useSafeArea = true
}) => {
const floating = useIsFloatingKeyboard();
@@ -36,6 +37,8 @@ const BaseDialog = ({
};
}, []);
const Wrapper = useSafeArea ? SafeAreaView : View;
return (
<Modal
visible={visible}
@@ -62,7 +65,7 @@ const BaseDialog = ({
onRequestClose && onRequestClose();
}}
>
<SafeAreaView
<Wrapper
style={{
backgroundColor: background ? background : transparent ? 'transparent' : 'rgba(0,0,0,0.3)'
}}
@@ -87,7 +90,7 @@ const BaseDialog = ({
{children}
</BouncingView>
</KeyboardAvoidingView>
</SafeAreaView>
</Wrapper>
</Modal>
);
};

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { ActivityIndicator, StyleSheet, View } from 'react-native';
import { useTracked } from '../../provider';
import { SIZE } from '../../utils/SizeUtils';
import { Button } from '../Button';
import Paragraph from '../Typography/Paragraph';
import { useThemeStore } from '../../stores/theme';
import { SIZE } from '../../utils/size';
import { Button } from '../ui/button';
import Paragraph from '../ui/typography/paragraph';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { notesnook } from '../../../e2e/test.ids';
@@ -16,8 +16,7 @@ const DialogButtons = ({
doneText,
positiveType
}) => {
const [state] = useTracked();
const { colors } = state;
const colors = useThemeStore(state => state.colors);
return (
<View

View File

@@ -1,12 +1,11 @@
import React from 'react';
import { View } from 'react-native';
import { useTracked } from '../../provider';
import { DDS } from '../../services/DeviceDetection';
import { useThemeStore } from '../../stores/theme';
import { DDS } from '../../services/device-detection';
import { getElevation } from '../../utils';
const DialogContainer = ({ width, height, ...restProps }) => {
const [state] = useTracked();
const colors = state.colors;
const colors = useThemeStore(state => state.colors);
return (
<View

View File

@@ -1,11 +1,28 @@
import React from 'react';
import { Text } from 'react-native';
import { View } from 'react-native';
import { useTracked } from '../../provider';
import { SIZE } from '../../utils/SizeUtils';
import { Button } from '../Button';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
import { useThemeStore } from '../../stores/theme';
import { SIZE } from '../../utils/size';
import { Button } from '../ui/button';
import { PressableButtonProps } from '../ui/pressable';
import Heading from '../ui/typography/heading';
import Paragraph from '../ui/typography/paragraph';
type DialogHeaderProps = {
icon?: string;
title?: string;
paragraph?: string;
button?: {
onPress?: () => void;
loading?: boolean;
title?: string;
type?: PressableButtonProps['type'];
};
paragraphColor?: string;
padding?: number;
centered?: boolean;
titlePart?: string;
};
const DialogHeader = ({
icon,
@@ -16,9 +33,8 @@ const DialogHeader = ({
padding,
centered,
titlePart
}) => {
const [state, dispatch] = useTracked();
const colors = state.colors;
}: DialogHeaderProps) => {
const colors = useThemeStore(state => state.colors);
return (
<>
@@ -43,7 +59,7 @@ const DialogHeader = ({
alignItems: 'center'
}}
>
<Heading size={SIZE.xl}>
<Heading style={{ textAlign: centered ? 'center' : 'left' }} size={SIZE.xl}>
{title} {titlePart ? <Text style={{ color: colors.accent }}>{titlePart}</Text> : null}
</Heading>
@@ -54,6 +70,7 @@ const DialogHeader = ({
borderRadius: 100,
paddingHorizontal: 12
}}
loading={button.loading}
fontSize={13}
title={button.title}
type={button.type || 'grayBg'}

View File

@@ -1,5 +1,5 @@
import { eSendEvent } from '../../services/EventManager';
import { eCloseSimpleDialog, eOpenSimpleDialog } from '../../utils/Events';
import { eSendEvent } from '../../services/event-manager';
import { eCloseSimpleDialog, eOpenSimpleDialog } from '../../utils/events';
type DialogInfo = {
title?: string;
@@ -25,7 +25,7 @@ type DialogInfo = {
context: 'global' | 'local';
};
export function presentDialog(data: DialogInfo): void {
export function presentDialog(data: Partial<DialogInfo>): void {
eSendEvent(eOpenSimpleDialog, data);
}

View File

@@ -0,0 +1,146 @@
import React, { useEffect, useRef, useState } from 'react';
import { View } from 'react-native';
import { useThemeStore } from '../../stores/theme';
import { DDS } from '../../services/device-detection';
import { eSubscribeEvent, eUnSubscribeEvent } from '../../services/event-manager';
import { getElevation } from '../../utils';
import { eCloseSimpleDialog, eOpenSimpleDialog } from '../../utils/events';
import { sleep } from '../../utils/time';
import Input from '../ui/input';
import Seperator from '../ui/seperator';
import { Toast } from '../toast';
import BaseDialog from './base-dialog';
import DialogButtons from './dialog-buttons';
import DialogHeader from './dialog-header';
export const Dialog = ({ context = 'global' }) => {
const colors = useThemeStore(state => state.colors);
const [visible, setVisible] = useState(false);
const [inputValue, setInputValue] = useState(null);
const inputRef = useRef();
const [dialogInfo, setDialogInfo] = useState({
title: '',
paragraph: '',
positiveText: 'Done',
negativeText: 'Cancel',
positivePress: () => {},
onClose: () => {},
positiveType: 'transparent',
icon: null,
paragraphColor: colors.pri,
input: false,
inputPlaceholder: 'Enter some text',
defaultValue: '',
disableBackdropClosing: false
});
useEffect(() => {
eSubscribeEvent(eOpenSimpleDialog, show);
eSubscribeEvent(eCloseSimpleDialog, hide);
return () => {
eUnSubscribeEvent(eOpenSimpleDialog, show);
eUnSubscribeEvent(eCloseSimpleDialog, hide);
};
}, []);
const onPressPositive = async () => {
if (dialogInfo.positivePress) {
inputRef.current?.blur();
let result = await dialogInfo.positivePress(inputValue || dialogInfo.defaultValue);
if (result === false) {
return;
}
}
hide();
};
const show = data => {
if (!data.context) data.context = 'global';
if (data.context !== context) return;
setDialogInfo(data);
setVisible(true);
};
const hide = () => {
setVisible(false);
};
const onNegativePress = async () => {
if (dialogInfo.onClose) {
await dialogInfo.onClose();
}
hide();
};
const style = {
...getElevation(5),
width: DDS.isTab ? 400 : '85%',
maxHeight: 450,
borderRadius: 5,
backgroundColor: colors.bg,
paddingTop: 12
};
return visible ? (
<BaseDialog
statusBarTranslucent={false}
bounce={!dialogInfo.input}
closeOnTouch={!dialogInfo.disableBackdropClosing}
onShow={async () => {
if (dialogInfo.input) {
inputRef.current?.setNativeProps({
text: dialogInfo.defaultValue
});
await sleep(300);
inputRef.current?.focus();
}
}}
visible={true}
onRequestClose={hide}
>
<View style={style}>
<DialogHeader
title={dialogInfo.title}
icon={dialogInfo.icon}
paragraph={dialogInfo.paragraph}
paragraphColor={dialogInfo.paragraphColor}
padding={12}
/>
<Seperator half />
{dialogInfo.input ? (
<View
style={{
paddingHorizontal: 12
}}
>
<Input
fwdRef={inputRef}
autoCapitalize="none"
onChangeText={value => {
setInputValue(value);
}}
secureTextEntry={dialogInfo.secureTextEntry}
//defaultValue={dialogInfo.defaultValue}
onSubmit={onPressPositive}
returnKeyLabel="Done"
returnKeyType="done"
placeholder={dialogInfo.inputPlaceholder}
/>
</View>
) : null}
<DialogButtons
onPressNegative={onNegativePress}
onPressPositive={dialogInfo.positivePress && onPressPositive}
positiveTitle={dialogInfo.positiveText}
negativeTitle={dialogInfo.negativeText}
positiveType={dialogInfo.positiveType}
/>
</View>
<Toast context="local" />
</BaseDialog>
) : null;
};

View File

@@ -1,15 +1,14 @@
import { eSendEvent } from '../../services/EventManager';
import { eSendEvent } from '../../services/event-manager';
import {
eCloseActionSheet,
eCloseAddNotebookDialog,
eCloseAddTopicDialog,
eCloseMoveNoteDialog,
eDispatchAction,
eOpenActionSheet,
eOpenAddNotebookDialog,
eOpenAddTopicDialog,
eOpenMoveNoteDialog
} from '../../utils/Events';
} from '../../utils/events';
export const ActionSheetEvent = (item, buttons) => {
eSendEvent(eOpenActionSheet, {
@@ -40,7 +39,3 @@ export const AddTopicEvent = topic => {
export const HideAddTopicEvent = notebook => {
eSendEvent(eCloseAddTopicDialog, notebook);
};
export const updateEvent = data => {
eSendEvent(eDispatchAction, data);
};

View File

@@ -0,0 +1,60 @@
import React from 'react';
import { useThemeStore } from '../../stores/theme';
import { EditorSettings } from '../../screens/editor/EditorSettings';
import { AddNotebookSheet } from '../sheets/add-notebook';
import { AddTopicDialog } from '../dialogs/add-topic';
import { AnnouncementDialog } from '../announcements';
import { AttachmentDialog } from '../attachments';
import Auth from '../auth';
import { SessionExpired } from '../auth/session-expired';
import { Dialog } from '../dialog';
import ExportNotesSheet from '../sheets/export-notes';
import ImagePreview from '../image-preview';
import MergeConflicts from '../merge-conflicts';
import AddToNotebookSheet from '../sheets/add-to';
import PremiumDialog from '../premium';
import { Expiring } from '../premium/expiring';
import PublishNoteSheet from '../sheets/publish-note';
import RateAppSheet from '../sheets/rate-app';
import RecoveryKeySheet from '../sheets/recovery-key';
import RestoreDataSheet from '../sheets/restore-data';
import ResultDialog from '../dialogs/result';
import SheetProvider from '../sheet-provider';
import ManageTagsSheet from '../sheets/manage-tags';
import { VaultDialog } from '../dialogs/vault';
const DialogProvider = React.memo(
() => {
const colors = useThemeStore(state => state.colors);
return (
<>
<Dialog context="global" />
<AddTopicDialog colors={colors} />
<AddNotebookSheet colors={colors} />
<PremiumDialog colors={colors} />
<Auth colors={colors} />
<MergeConflicts />
<ExportNotesSheet />
<RecoveryKeySheet colors={colors} />
<SheetProvider />
<RestoreDataSheet />
<ResultDialog />
<VaultDialog colors={colors} />
<AddToNotebookSheet colors={colors} />
<RateAppSheet />
<ImagePreview />
<EditorSettings />
<PublishNoteSheet />
<ManageTagsSheet />
<AttachmentDialog />
<Expiring />
<AnnouncementDialog />
<SessionExpired />
</>
);
},
() => true
);
export default DialogProvider;

View File

@@ -1,19 +1,19 @@
import React, { createRef } from 'react';
import { Keyboard, LayoutAnimation, UIManager, View } from 'react-native';
import { Transition, Transitioning, TransitioningView } from 'react-native-reanimated';
import { useMenuStore } from '../../provider/stores';
import { eSubscribeEvent, eUnSubscribeEvent, ToastEvent } from '../../services/EventManager';
import Navigation from '../../services/Navigation';
import { db } from '../../utils/database';
import { eCloseAddTopicDialog, eOpenAddTopicDialog } from '../../utils/Events';
import { sleep } from '../../utils/TimeUtils';
import BaseDialog from '../Dialog/base-dialog';
import DialogButtons from '../Dialog/dialog-buttons';
import DialogContainer from '../Dialog/dialog-container';
import DialogHeader from '../Dialog/dialog-header';
import Input from '../Input';
import Seperator from '../Seperator';
import { Toast } from '../Toast';
import { useMenuStore } from '../../../stores/stores';
import { eSubscribeEvent, eUnSubscribeEvent, ToastEvent } from '../../../services/event-manager';
import Navigation from '../../../services/navigation';
import { db } from '../../../utils/database';
import { eCloseAddTopicDialog, eOpenAddTopicDialog } from '../../../utils/events';
import { sleep } from '../../../utils/time';
import BaseDialog from '../../dialog/base-dialog';
import DialogButtons from '../../dialog/dialog-buttons';
import DialogContainer from '../../dialog/dialog-container';
import DialogHeader from '../../dialog/dialog-header';
import Input from '../../ui/input';
import Seperator from '../../ui/seperator';
import { Toast } from '../../toast';
export class AddTopicDialog extends React.Component {
constructor(props) {

View File

@@ -1,23 +1,20 @@
import React, { useEffect, useState } from 'react';
import { ScrollView, View } from 'react-native';
import BaseDialog from '../../components/Dialog/base-dialog';
import { PressableButton } from '../../components/PressableButton';
import Seperator from '../../components/Seperator';
import { useTracked } from '../../provider';
import { useMessageStore } from '../../provider/stores';
import { DDS } from '../../services/DeviceDetection';
import { eSubscribeEvent, eUnSubscribeEvent } from '../../services/EventManager';
import { getElevation } from '../../utils';
import { eCloseJumpToDialog, eOpenJumpToDialog, eScrollEvent } from '../../utils/Events';
import { SIZE } from '../../utils/SizeUtils';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
import { useThemeStore } from '../../../stores/theme';
import { useMessageStore } from '../../../stores/stores';
import { DDS } from '../../../services/device-detection';
import { eSubscribeEvent, eUnSubscribeEvent } from '../../../services/event-manager';
import { getElevation } from '../../../utils';
import { eCloseJumpToDialog, eOpenJumpToDialog, eScrollEvent } from '../../../utils/events';
import { SIZE } from '../../../utils/size';
import BaseDialog from '../../dialog/base-dialog';
import { PressableButton } from '../../ui/pressable';
import Paragraph from '../../ui/typography/paragraph';
const offsets = [];
let timeout = null;
const JumpToDialog = ({ scrollRef, data, type, screen }) => {
const [state] = useTracked();
const { colors } = state;
const JumpToSectionDialog = ({ scrollRef, data, type, screen }) => {
const colors = useThemeStore(state => state.colors);
const notes = data;
const [visible, setVisible] = useState(false);
const [currentIndex, setCurrentIndex] = useState(null);
@@ -157,4 +154,4 @@ const JumpToDialog = ({ scrollRef, data, type, screen }) => {
);
};
export default JumpToDialog;
export default JumpToSectionDialog;

View File

@@ -1,15 +1,18 @@
import React from 'react';
import { View } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { useTracked } from '../../provider';
import { eSendEvent } from '../../services/EventManager';
import { eCloseProgressDialog, eCloseResultDialog, eOpenPremiumDialog } from '../../utils/Events';
import { SIZE } from '../../utils/SizeUtils';
import { sleep } from '../../utils/TimeUtils';
import Paragraph from '../Typography/Paragraph';
import { useThemeStore } from '../../../stores/theme';
import { eSendEvent } from '../../../services/event-manager';
import {
eCloseProgressDialog,
eCloseResultDialog,
eOpenPremiumDialog
} from '../../../utils/events';
import { SIZE } from '../../../utils/size';
import { sleep } from '../../../utils/time';
import Paragraph from '../../ui/typography/paragraph';
export const ProFeatures = ({ count = 6 }) => {
const [state, dispatch] = useTracked();
const { colors } = state;
const colors = useThemeStore(state => state.colors);
return (
<>

View File

@@ -1,21 +1,20 @@
import React, { useEffect, useState } from 'react';
import { View } from 'react-native';
import { useTracked } from '../../provider';
import { DDS } from '../../services/DeviceDetection';
import { eSubscribeEvent, eUnSubscribeEvent } from '../../services/EventManager';
import { getElevation } from '../../utils';
import { eCloseResultDialog, eOpenResultDialog } from '../../utils/Events';
import { SIZE } from '../../utils/SizeUtils';
import { Button } from '../Button';
import BaseDialog from '../Dialog/base-dialog';
import Seperator from '../Seperator';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
import { useThemeStore } from '../../../stores/theme';
import { DDS } from '../../../services/device-detection';
import { eSubscribeEvent, eUnSubscribeEvent } from '../../../services/event-manager';
import { getElevation } from '../../../utils';
import { eCloseResultDialog, eOpenResultDialog } from '../../../utils/events';
import { SIZE } from '../../../utils/size';
import { Button } from '../../ui/button';
import BaseDialog from '../../dialog/base-dialog';
import Seperator from '../../ui/seperator';
import Heading from '../../ui/typography/heading';
import Paragraph from '../../ui/typography/paragraph';
import { ProFeatures } from './pro-features';
const ResultDialog = () => {
const [state, dispatch] = useTracked();
const { colors } = state;
const colors = useThemeStore(state => state.colors);
const [visible, setVisible] = useState(false);
const [dialogData, setDialogData] = useState({
title: 'Thank you for signing up!',

View File

@@ -2,37 +2,38 @@ import Clipboard from '@react-native-clipboard/clipboard';
import React, { Component, createRef } from 'react';
import { InteractionManager, View } from 'react-native';
import Share from 'react-native-share';
import { notesnook } from '../../../e2e/test.ids';
import BiometricService from '../../services/BiometricService';
import { DDS } from '../../services/DeviceDetection';
import { notesnook } from '../../../../e2e/test.ids';
import BiometricService from '../../../services/biometrics';
import { DDS } from '../../../services/device-detection';
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
} from '../../services/EventManager';
import Navigation from '../../services/Navigation';
import { getElevation, toTXT } from '../../utils';
import { db } from '../../utils/database';
} from '../../../services/event-manager';
import Navigation from '../../../services/navigation';
import { getElevation, toTXT } from '../../../utils';
import { db } from '../../../utils/database';
import {
eClearEditor,
eCloseActionSheet,
eCloseVaultDialog,
eOnLoadNote,
eOpenVaultDialog
} from '../../utils/Events';
import { deleteItems } from '../../utils/functions';
import { tabBarRef } from '../../utils/Refs';
import { sleep } from '../../utils/TimeUtils';
import { getNote } from '../../views/Editor/Functions';
import { Button } from '../Button';
import BaseDialog from '../Dialog/base-dialog';
import DialogButtons from '../Dialog/dialog-buttons';
import DialogHeader from '../Dialog/dialog-header';
import Input from '../Input';
import Seperator from '../Seperator';
import { Toast } from '../Toast';
import Paragraph from '../Typography/Paragraph';
} from '../../../utils/events';
import { deleteItems } from '../../../utils/functions';
import { tabBarRef } from '../../../utils/global-refs';
import { sleep } from '../../../utils/time';
import { getNote } from '../../../screens/editor/Functions';
import { Button } from '../../ui/button';
import BaseDialog from '../../dialog/base-dialog';
import DialogButtons from '../../dialog/dialog-buttons';
import DialogHeader from '../../dialog/dialog-header';
import Input from '../../ui/input';
import Seperator from '../../ui/seperator';
import { Toast } from '../../toast';
import Paragraph from '../../ui/typography/paragraph';
import SearchService from '../../../services/search';
let Keychain;
const passInputRef = createRef();
@@ -124,7 +125,7 @@ export class VaultDialog extends Component {
/**
*
* @param {import('../../services/EventManager').vaultType} data
* @param {import('../../../services/event-manager').vaultType} data
*/
open = async data => {
if (!Keychain) {
@@ -199,6 +200,7 @@ export class VaultDialog extends Component {
]);
this.password = null;
this.confirmPassword = null;
SearchService.updateAndSearch();
this.setState({
visible: false,
note: {},
@@ -703,7 +705,7 @@ export class VaultDialog extends Component {
onSubmit={() => {
changePassword ? changePassInputRef.current?.focus() : this.onPress;
}}
autoCompleteType="password"
autoComplete="password"
returnKeyLabel={changePassword ? 'Next' : this.state.title}
returnKeyType={changePassword ? 'next' : 'done'}
secureTextEntry
@@ -755,7 +757,7 @@ export class VaultDialog extends Component {
onChangeText={value => {
this.newPassword = value;
}}
autoCompleteType="password"
autoComplete="password"
onSubmit={this.onPress}
returnKeyLabel="Change"
returnKeyType="done"
@@ -774,7 +776,7 @@ export class VaultDialog extends Component {
onChangeText={value => {
this.password = value;
}}
autoCompleteType="password"
autoComplete="password"
returnKeyLabel="Next"
returnKeyType="next"
secureTextEntry
@@ -794,7 +796,7 @@ export class VaultDialog extends Component {
errorMessage="Passwords do not match."
onErrorCheck={e => null}
marginBottom={0}
autoCompleteType="password"
autoComplete="password"
returnKeyLabel="Create"
returnKeyType="done"
onChangeText={value => {

View File

@@ -1,20 +1,19 @@
import React, { useEffect, useState } from 'react';
import { Platform, StyleSheet, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTracked } from '../../provider';
import { eSubscribeEvent, eUnSubscribeEvent } from '../../services/EventManager';
import SearchService from '../../services/SearchService';
import { eScrollEvent } from '../../utils/Events';
import { ActionIcon } from '../ActionIcon';
import { useThemeStore } from '../../stores/theme';
import { eSubscribeEvent, eUnSubscribeEvent } from '../../services/event-manager';
import SearchService from '../../services/search';
import { eScrollEvent } from '../../utils/events';
import { IconButton } from '../ui/icon-button';
import { SearchInput } from '../SearchInput';
import { HeaderLeftMenu } from './HeaderLeftMenu';
import { HeaderRightMenu } from './HeaderRightMenu';
import { LeftMenus } from './left-menus';
import { RightMenus } from './right-menus';
import { Title } from './title';
export const Header = React.memo(
({ root, title, screen, isBack, color, action, rightButtons, notebook }) => {
const [state] = useTracked();
const { colors } = state;
const colors = useThemeStore(state => state.colors);
const insets = useSafeAreaInsets();
const [hide, setHide] = useState(true);
@@ -49,7 +48,7 @@ export const Header = React.memo(
]}
>
<View style={styles.leftBtnContainer}>
<HeaderLeftMenu headerMenuState={!isBack} currentScreen={screen} />
<LeftMenus headerMenuState={!isBack} currentScreen={screen} />
<Title
notebook={notebook}
@@ -60,7 +59,7 @@ export const Header = React.memo(
/>
</View>
<HeaderRightMenu rightButtons={rightButtons} action={action} currentScreen={screen} />
<RightMenus rightButtons={rightButtons} action={action} currentScreen={screen} />
</View>
);
},

View File

@@ -1,15 +1,13 @@
import React from 'react';
import { notesnook } from '../../../e2e/test.ids';
import { useTracked } from '../../provider';
import { useSettingStore } from '../../provider/stores';
import { DDS } from '../../services/DeviceDetection';
import Navigation from '../../services/Navigation';
import { SIZE } from '../../utils/SizeUtils';
import { ActionIcon } from '../ActionIcon';
import { useThemeStore } from '../../stores/theme';
import { useSettingStore } from '../../stores/stores';
import { DDS } from '../../services/device-detection';
import Navigation from '../../services/navigation';
import { IconButton } from '../ui/icon-button';
export const HeaderLeftMenu = ({ currentScreen, headerMenuState }) => {
const [state] = useTracked();
const { colors } = state;
export const LeftMenus = ({ currentScreen, headerMenuState }) => {
const colors = useThemeStore(state => state.colors);
const deviceMode = useSettingStore(state => state.deviceMode);
const onLeftButtonPress = () => {
@@ -23,7 +21,7 @@ export const HeaderLeftMenu = ({ currentScreen, headerMenuState }) => {
return (
<>
{deviceMode !== 'tablet' || currentScreen === 'Search' || !headerMenuState ? (
<ActionIcon
<IconButton
testID={notesnook.ids.default.header.buttons.left}
customStyle={{
justifyContent: 'center',

View File

@@ -1,25 +1,22 @@
import React, { useRef } from 'react';
import { StyleSheet, View } from 'react-native';
import Menu, { MenuItem } from 'react-native-reanimated-material-menu';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import Menu from 'react-native-reanimated-material-menu';
import { notesnook } from '../../../e2e/test.ids';
import { useTracked } from '../../provider';
import { useSettingStore } from '../../provider/stores';
import Navigation from '../../services/Navigation';
import { SIZE } from '../../utils/SizeUtils';
import { sleep } from '../../utils/TimeUtils';
import { ActionIcon } from '../ActionIcon';
import { Button } from '../Button';
import { useThemeStore } from '../../stores/theme';
import { useSettingStore } from '../../stores/stores';
import Navigation from '../../services/navigation';
import { SIZE } from '../../utils/size';
import { Button } from '../ui/button';
import { IconButton } from '../ui/icon-button';
export const HeaderRightMenu = ({ currentScreen, action, rightButtons }) => {
const [state] = useTracked();
const { colors } = state;
export const RightMenus = ({ currentScreen, action, rightButtons }) => {
const colors = useThemeStore(state => state.colors);
const deviceMode = useSettingStore(state => state.deviceMode);
const menuRef = useRef();
return (
<View style={styles.rightBtnContainer}>
{currentScreen !== 'Settings' ? (
<ActionIcon
<IconButton
onPress={async () => {
Navigation.navigate('Search', {
menu: false
@@ -65,7 +62,7 @@ export const HeaderRightMenu = ({ currentScreen, action, rightButtons }) => {
backgroundColor: colors.bg
}}
button={
<ActionIcon
<IconButton
onPress={() => {
menuRef.current?.show();
}}
@@ -77,21 +74,20 @@ export const HeaderRightMenu = ({ currentScreen, action, rightButtons }) => {
}
>
{rightButtons.map((item, index) => (
<MenuItem
<Button
style={{
width: 150,
justifyContent: 'flex-start',
borderRadius: 0
}}
type="gray"
buttonType={{
text: colors.pri
}}
key={item.title}
onPress={async () => {
menuRef.current?.hide();
await sleep(300);
item.func();
}}
textStyle={{
fontSize: SIZE.md,
color: colors.pri
}}
>
<Icon name={item.icon} size={SIZE.md} />
{' ' + item.title}
</MenuItem>
title={item.title}
onPress={item.func}
/>
))}
</Menu>
) : null}

View File

@@ -1,16 +1,15 @@
import React, { useEffect, useState } from 'react';
import { View } from 'react-native';
import { useTracked } from '../../provider';
import { eSendEvent, eSubscribeEvent, eUnSubscribeEvent } from '../../services/EventManager';
import Navigation from '../../services/Navigation';
import { eOnNewTopicAdded, eScrollEvent } from '../../utils/Events';
import { SIZE } from '../../utils/SizeUtils';
import Heading from '../Typography/Heading';
import Paragraph from '../Typography/Paragraph';
import { useThemeStore } from '../../stores/theme';
import { eSendEvent, eSubscribeEvent, eUnSubscribeEvent } from '../../services/event-manager';
import Navigation from '../../services/navigation';
import { eOnNewTopicAdded, eScrollEvent } from '../../utils/events';
import { SIZE } from '../../utils/size';
import Heading from '../ui/typography/heading';
import Paragraph from '../ui/typography/paragraph';
export const Title = ({ heading, headerColor, screen, notebook }) => {
const [state] = useTracked();
const { colors } = state;
const colors = useThemeStore(state => state.colors);
const [hide, setHide] = useState(screen === 'Notebook' ? true : false);
const onScroll = data => {

View File

@@ -1,11 +1,9 @@
import React, { useEffect, useState } from 'react';
import { View } from 'react-native';
import ImageViewer from 'react-native-image-zoom-viewer';
import RNFetchBlob from 'rn-fetch-blob';
import Storage from '../../utils/storage';
import { ActionIcon } from '../ActionIcon';
import BaseDialog from '../Dialog/base-dialog';
const { eSubscribeEvent, eUnSubscribeEvent } = require('../../services/EventManager');
import { eSubscribeEvent, eUnSubscribeEvent } from '../../services/event-manager';
import BaseDialog from '../dialog/base-dialog';
import { IconButton } from '../ui/icon-button';
const ImagePreview = () => {
const [visible, setVisible] = useState(false);
@@ -62,7 +60,7 @@ const ImagePreview = () => {
paddingTop: 30
}}
>
<ActionIcon
<IconButton
name="close"
color="white"
onPress={() => {

View File

@@ -0,0 +1,365 @@
import React, { useEffect, useRef, useState } from 'react';
import { NativeModules, Platform, StatusBar, View } from 'react-native';
import RNBootSplash from 'react-native-bootsplash';
import { checkVersion } from 'react-native-check-version';
import SettingsBackupAndRestore from '../../screens/settings/backup-restore';
import BiometricService from '../../services/biometrics';
import { DDS } from '../../services/device-detection';
import { eSendEvent, presentSheet, ToastEvent } from '../../services/event-manager';
import { setRateAppMessage } from '../../services/message';
import PremiumService from '../../services/premium';
import SettingsService from '../../services/settings';
import {
initialize,
useFavoriteStore,
useMessageStore,
useNoteStore,
useSettingStore,
useUserStore
} from '../../stores/stores';
import { useThemeStore } from '../../stores/theme';
import { editing } from '../../utils';
import { db } from '../../utils/database';
import { MMKV } from '../../utils/database/mmkv';
import { eOpenAnnouncementDialog } from '../../utils/events';
import { tabBarRef } from '../../utils/global-refs';
import { SIZE } from '../../utils/size';
import { sleep } from '../../utils/time';
import { SVG } from '../auth/background';
import Intro from '../intro';
import { Update } from '../sheets/update';
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';
import NewFeature from '../sheets/new-feature/index';
const Launcher = React.memo(
() => {
const colors = useThemeStore(state => state.colors);
const setNotes = useNoteStore(state => state.setNotes);
const setFavorites = useFavoriteStore(state => state.setFavorites);
const setLoading = useNoteStore(state => state.setLoading);
const loading = useNoteStore(state => state.loading);
const user = useUserStore(state => state.user);
const verifyUser = useUserStore(state => state.verifyUser);
const setVerifyUser = useUserStore(state => state.setVerifyUser);
const deviceMode = useSettingStore(state => state.deviceMode);
const passwordInputRef = useRef();
const password = useRef();
const [requireIntro, setRequireIntro] = useState({
updated: false,
value: false
});
const dbInitCompleted = useRef(false);
const loadNotes = async () => {
if (verifyUser) {
return;
}
await restoreEditorState();
await db.notes.init();
setNotes();
setFavorites();
setLoading(false);
Walkthrough.init();
};
const init = async () => {
if (!dbInitCompleted.current) {
await db.init();
initialize();
useUserStore.getState().setUser(await db.user.getUser());
dbInitCompleted.current = true;
}
if (!verifyUser) {
loadNotes();
}
};
const hideSplashScreen = async () => {
await sleep(requireIntro.value ? 500 : 0);
await RNBootSplash.hide({ fade: true });
setTimeout(async () => {
if (Platform.OS === 'android') {
NativeModules.RNBars.setStatusBarStyle(!colors.night ? 'light-content' : 'dark-content');
await sleep(5);
NativeModules.RNBars.setStatusBarStyle(colors.night ? 'light-content' : 'dark-content');
} else {
StatusBar.setBarStyle(colors.night ? 'light-content' : 'dark-content');
}
}, 500);
};
useEffect(() => {
if (requireIntro.updated) {
hideSplashScreen();
}
}, [requireIntro, verifyUser]);
useEffect(() => {
let introCompleted = SettingsService.get().introCompleted;
setRequireIntro({
updated: true,
value: !introCompleted
});
}, []);
useEffect(() => {
if (!loading) {
doAppLoadActions();
}
return () => {
dbInitCompleted.current = false;
};
}, [loading]);
const doAppLoadActions = async () => {
await sleep(500);
if (SettingsService.get().sessionExpired) {
eSendEvent('session_expired');
return;
}
if (NewFeature.present()) return;
if (await checkAppUpdateAvailable()) return;
if (await checkForRateAppRequest()) return;
if (await checkNeedsBackup()) return;
if (await PremiumService.getRemainingTrialDaysStatus()) return;
await useMessageStore.getState().setAnnouncement();
if (!requireIntro?.value) {
useMessageStore.subscribe(state => {
let dialogs = state.dialogs;
if (dialogs.length > 0) {
eSendEvent(eOpenAnnouncementDialog, dialogs[0]);
}
});
}
};
const checkAppUpdateAvailable = async () => {
try {
const version = await checkVersion();
if (!version.needsUpdate) return false;
presentSheet({
component: ref => <Update version={version} fwdRef={ref} />
});
return true;
} catch (e) {
return false;
}
};
const restoreEditorState = async () => {
let appState = await MMKV.getItem('appState');
if (appState) {
appState = JSON.parse(appState);
if (
appState.note &&
!appState.note.locked &&
!appState.movedAway &&
Date.now() < appState.timestamp + 3600000
) {
editing.isRestoringState = true;
editing.currentlyEditing = true;
editing.movedAway = false;
if (!DDS.isTab) {
tabBarRef.current?.goToPage(1);
}
eSendEvent('loadingNote', appState.note);
}
}
};
const checkForRateAppRequest = async () => {
let rateApp = SettingsService.get().rateApp;
if (rateApp && rateApp < Date.now() && !useMessageStore.getState().message?.visible) {
setRateAppMessage();
return true;
}
return false;
};
const checkNeedsBackup = async () => {
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 = async () => {
if (!(await BiometricService.isBiometryAvailable())) {
ToastEvent.show({
heading: 'Biometrics unavailable',
message: 'Try unlocking the app with your account password'
});
return;
}
let verified = await BiometricService.validateUser('Unlock to access your notes', '');
if (verified) {
setVerifyUser(false);
password.current = null;
}
};
useEffect(() => {
if (verifyUser) {
onUnlockBiometrics();
}
init();
}, [verifyUser]);
const onSubmit = async () => {
if (!password.current) return;
try {
let verified = await db.user.verifyPassword(password.current);
if (verified) {
setVerifyUser(false);
password.current = null;
}
} catch (e) {}
};
return verifyUser ? (
<View
style={{
backgroundColor: colors.bg,
width: '100%',
height: '100%',
position: 'absolute',
zIndex: 999
}}
>
<View
style={{
height: 250,
overflow: 'hidden'
}}
>
<SvgView src={SVG(colors.night ? 'white' : 'black')} height={700} />
</View>
<View
style={{
flex: 1,
justifyContent: 'center',
width: deviceMode !== 'mobile' ? '50%' : Platform.OS == 'ios' ? '95%' : '100%',
paddingHorizontal: 12,
marginBottom: 30,
marginTop: 15
}}
>
<IconButton
name="fingerprint"
size={100}
customStyle={{
width: 100,
height: 100,
marginBottom: 20,
marginTop: user ? 0 : 50
}}
onPress={onUnlockBiometrics}
color={colors.border}
/>
<Heading
color={colors.heading}
style={{
alignSelf: 'center',
textAlign: 'center'
}}
>
Unlock to access your notes
</Heading>
<Paragraph
style={{
alignSelf: 'center',
textAlign: 'center',
fontSize: SIZE.md,
maxWidth: '90%'
}}
>
Please verify it's you
</Paragraph>
<Seperator />
<View
style={{
width: '100%',
padding: 12,
backgroundColor: colors.bg,
flexGrow: 1
}}
>
{user ? (
<>
<Input
fwdRef={passwordInputRef}
secureTextEntry
placeholder="Enter account password"
onChangeText={v => (password.current = v)}
onSubmit={onSubmit}
/>
</>
) : null}
<View
style={{
marginTop: user ? 50 : 25
}}
>
{user ? (
<>
<Button
title="Continue"
type="accent"
onPress={onSubmit}
width={250}
height={45}
style={{
borderRadius: 150,
marginBottom: 10
}}
fontSize={SIZE.md}
/>
</>
) : null}
<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>
</View>
) : requireIntro.value && !loading ? (
<Intro />
) : null;
},
() => true
);
export default Launcher;

View File

@@ -1,16 +1,15 @@
import React from 'react';
import { View } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { useTracked } from '../../provider';
import { useMessageStore, useSelectionStore } from '../../provider/stores';
import { hexToRGBA } from '../../utils/ColorUtils';
import { SIZE } from '../../utils/SizeUtils';
import { PressableButton } from '../PressableButton';
import Paragraph from '../Typography/Paragraph';
import { useThemeStore } from '../../stores/theme';
import { useMessageStore, useSelectionStore } from '../../stores/stores';
import { hexToRGBA } from '../../utils/color-scheme/utils';
import { SIZE } from '../../utils/size';
import { PressableButton } from '../ui/pressable';
import Paragraph from '../ui/typography/paragraph';
export const Card = ({ color }) => {
const [state] = useTracked();
const colors = state.colors;
const colors = useThemeStore(state => state.colors);
color = color ? color : colors.accent;
const selectionMode = useSelectionStore(state => state.selectionMode);

View File

@@ -0,0 +1,130 @@
import React from 'react';
import { ActivityIndicator, useWindowDimensions, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useThemeStore } from '../../stores/theme';
import { useSettingStore } from '../../stores/stores';
import { useTip } from '../../services/tip-manager';
import { COLORS_NOTE } from '../../utils/color-scheme';
import { SIZE } from '../../utils/size';
import { Button } from '../ui/button';
import Seperator from '../ui/seperator';
import { Tip } from '../tip';
import Heading from '../ui/typography/heading';
import Paragraph from '../ui/typography/paragraph';
export const Empty = React.memo(
({ loading = true, placeholderData, headerProps, type, screen }) => {
const colors = useThemeStore(state => state.colors);
const insets = useSafeAreaInsets();
const { height } = useWindowDimensions();
const introCompleted = useSettingStore(state => state.settings.introCompleted);
const tip = useTip(
screen === 'Notes' && introCompleted ? 'first-note' : placeholderData.type || type,
screen === 'Notes' ? 'notes' : null
);
const color =
colors[COLORS_NOTE[headerProps.color?.toLowerCase()] ? headerProps.color : 'accent'];
return (
<View
style={[
{
height: height - (140 + insets.top),
width: '80%',
justifyContent: 'center',
alignSelf: 'center'
}
]}
>
{!loading ? (
<>
<Tip
color={COLORS_NOTE[headerProps.color?.toLowerCase()] ? headerProps.color : 'accent'}
tip={tip || { text: placeholderData.paragraph }}
style={{
backgroundColor: 'transparent',
paddingHorizontal: 0
}}
/>
{placeholderData.button && (
<Button
type="grayAccent"
title={placeholderData.button}
iconPosition="right"
icon="arrow-right"
onPress={placeholderData.action}
accentColor={
COLORS_NOTE[headerProps.color?.toLowerCase()] ? headerProps.color : 'accent'
}
accentText="light"
style={{
alignSelf: 'flex-start',
borderRadius: 5,
height: 40
}}
/>
)}
</>
) : (
<>
<View
style={{
alignSelf: 'center',
alignItems: 'flex-start',
width: '100%'
}}
>
<Heading>{placeholderData.heading}</Heading>
<Paragraph size={SIZE.sm} textBreakStrategy="balanced">
{placeholderData.loading}
</Paragraph>
<Seperator />
<ActivityIndicator
size={SIZE.lg}
color={COLORS_NOTE[headerProps.color?.toLowerCase()] || colors.accent}
/>
</View>
</>
)}
</View>
);
},
(prev, next) => {
if (prev.loading === next.loading) return true;
return false;
}
);
/**
* Make a tips manager.
* The tip manager stores many tips. Each tip has following values
* 1. Text
* 2. contexts: An array of context strings. // Places where the tip can be shown
* 3. Button if any.
* 4. Image/Gif asset.
*
* Tip manager adds the following methods -> get(context). Returns a random tip for the following context.
*
* Tips can be shown in a sheet or in a list. For sheets, GeneralSheet can be used to
* render tips.
*
* Where can the tips be shown and how?
* 1. When transitioning, show tips in a sheet. Make sure its useful
* 2. Replace placeholders with tips.
* 3. Show tips in editor placeholder.
* 4. Show tips between list items?
*
* Tooltips.
* Small tooltips can be shown in initial render first time.
* Especially for items that are not shown on blank page. Should be
* in places where it makes sense and does not interrupt the user.
*
* Can also be shown when first time entering a screen that
* has something that the user might not know of. Like sorting and side menu.
*
* Todo:
* 1. Make a tip manager.
* 2. Make a list of tips.
* 3. Add images for those tips.
* 4. Show tips
*/

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