mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-28 16:06:47 +01:00
Merge branch 'develop' into tiptap-editor
This commit is contained in:
1
apps/mobile/.env
Normal file
1
apps/mobile/.env
Normal file
@@ -0,0 +1 @@
|
||||
GITHUB_RELEASE=false
|
||||
1
apps/mobile/.env.public
Normal file
1
apps/mobile/.env.public
Normal file
@@ -0,0 +1 @@
|
||||
GITHUB_RELEASE=true
|
||||
@@ -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',
|
||||
{},
|
||||
|
||||
71
apps/mobile/.github/workflows/android-public-release.yml
vendored
Normal file
71
apps/mobile/.github/workflows/android-public-release.yml
vendored
Normal 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
|
||||
2
apps/mobile/.gitignore
vendored
2
apps/mobile/.gitignore
vendored
@@ -2,6 +2,8 @@
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
*Issues.md
|
||||
|
||||
# Xcode
|
||||
#
|
||||
build/
|
||||
|
||||
1
apps/mobile/.vscode/settings.json
vendored
Normal file
1
apps/mobile/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
4
apps/mobile/android/app/proguard-rules.pro
vendored
4
apps/mobile/android/app/proguard-rules.pro
vendored
@@ -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 { *; }
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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!
|
||||
@@ -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
8
apps/mobile/features.ts
Normal 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'
|
||||
}
|
||||
];
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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!
|
||||
11
apps/mobile/releasenotes.md
Normal file
11
apps/mobile/releasenotes.md
Normal 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!
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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
|
||||
);
|
||||
@@ -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;
|
||||
}
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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 }) =>
|
||||
@@ -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
|
||||
146
apps/mobile/src/components/announcements/cta.js
Normal file
146
apps/mobile/src/components/announcements/cta.js
Normal 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>
|
||||
);
|
||||
};
|
||||
21
apps/mobile/src/components/announcements/description.js
Normal file
21
apps/mobile/src/components/announcements/description.js
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
72
apps/mobile/src/components/announcements/title.js
Normal file
72
apps/mobile/src/components/announcements/title.js
Normal 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>
|
||||
);
|
||||
};
|
||||
327
apps/mobile/src/components/attachments/actions.js
Normal file
327
apps/mobile/src/components/attachments/actions.js
Normal 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;
|
||||
144
apps/mobile/src/components/attachments/attachmentitem.js
Normal file
144
apps/mobile/src/components/attachments/attachmentitem.js
Normal 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>
|
||||
);
|
||||
};
|
||||
176
apps/mobile/src/components/attachments/index.js
Normal file
176
apps/mobile/src/components/attachments/index.js
Normal 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>
|
||||
);
|
||||
};
|
||||
2
apps/mobile/src/components/auth/background.js
Normal file
2
apps/mobile/src/components/auth/background.js
Normal 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>`;
|
||||
130
apps/mobile/src/components/auth/changepassword.js
Normal file
130
apps/mobile/src/components/auth/changepassword.js
Normal 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 />
|
||||
});
|
||||
};
|
||||
157
apps/mobile/src/components/auth/forgotpassword.js
Normal file
157
apps/mobile/src/components/auth/forgotpassword.js
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
73
apps/mobile/src/components/auth/index.js
Normal file
73
apps/mobile/src/components/auth/index.js
Normal 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;
|
||||
283
apps/mobile/src/components/auth/login.js
Normal file
283
apps/mobile/src/components/auth/login.js
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
260
apps/mobile/src/components/auth/sessionexpired.js
Normal file
260
apps/mobile/src/components/auth/sessionexpired.js
Normal 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>
|
||||
)
|
||||
);
|
||||
};
|
||||
293
apps/mobile/src/components/auth/signup.js
Normal file
293
apps/mobile/src/components/auth/signup.js
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
284
apps/mobile/src/components/auth/twofactor.js
Normal file
284
apps/mobile/src/components/auth/twofactor.js
Normal 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;
|
||||
@@ -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 ? (
|
||||
@@ -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, {
|
||||
@@ -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
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
146
apps/mobile/src/components/dialog/index.js
Normal file
146
apps/mobile/src/components/dialog/index.js
Normal 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;
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
60
apps/mobile/src/components/dialogprovider/index.js
Normal file
60
apps/mobile/src/components/dialogprovider/index.js
Normal 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;
|
||||
@@ -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) {
|
||||
@@ -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;
|
||||
@@ -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 (
|
||||
<>
|
||||
@@ -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!',
|
||||
@@ -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 => {
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
@@ -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',
|
||||
@@ -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}
|
||||
@@ -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 => {
|
||||
@@ -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={() => {
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
365
apps/mobile/src/components/launcher/index.js
Normal file
365
apps/mobile/src/components/launcher/index.js
Normal 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;
|
||||
@@ -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);
|
||||
130
apps/mobile/src/components/list/empty.js
Normal file
130
apps/mobile/src/components/list/empty.js
Normal 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
Reference in New Issue
Block a user