├── .eslintrc.js ├── .expo-shared └── assets.json ├── .github └── FUNDING.yml ├── .gitignore ├── .watchmanconfig ├── AUTHORS ├── App.ts ├── LICENSES └── GPL-3.0-only ├── README.md ├── android ├── app │ ├── build.gradle │ ├── expo.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── assets │ │ └── kernel-manifest.json │ │ ├── java │ │ └── host │ │ │ └── exp │ │ │ └── exponent │ │ │ ├── MainActivity.java │ │ │ ├── MainApplication.java │ │ │ └── generated │ │ │ ├── AppConstants.java │ │ │ └── DetachBuildConstants.java │ │ └── res │ │ ├── drawable-hdpi │ │ ├── ic_arrow_back_white_36dp.png │ │ ├── ic_home_white_36dp.png │ │ ├── ic_logo_white_32dp.png │ │ ├── ic_refresh_white_36dp.png │ │ └── ic_share_white_36dp.png │ │ ├── drawable-mdpi │ │ ├── ic_arrow_back_white_36dp.png │ │ ├── ic_home_white_36dp.png │ │ ├── ic_logo_white_32dp.png │ │ ├── ic_refresh_white_36dp.png │ │ └── ic_share_white_36dp.png │ │ ├── drawable-xhdpi │ │ ├── ic_arrow_back_white_36dp.png │ │ ├── ic_home_white_36dp.png │ │ ├── ic_logo_white_32dp.png │ │ ├── ic_refresh_white_36dp.png │ │ └── ic_share_white_36dp.png │ │ ├── drawable-xxhdpi │ │ ├── ic_arrow_back_white_36dp.png │ │ ├── ic_home_white_36dp.png │ │ ├── ic_logo_white_32dp.png │ │ ├── ic_refresh_white_36dp.png │ │ └── ic_share_white_36dp.png │ │ ├── drawable-xxxhdpi │ │ ├── big_logo_dark.png │ │ ├── big_logo_dark_filled.png │ │ ├── big_logo_filled.png │ │ ├── big_logo_new_filled.png │ │ ├── ic_arrow_back_white_36dp.png │ │ ├── ic_home_white_36dp.png │ │ ├── ic_logo_white_32dp.png │ │ ├── ic_refresh_white_36dp.png │ │ ├── ic_share_white_36dp.png │ │ ├── notification_icon.png │ │ ├── pin_white.png │ │ ├── pin_white_fade.png │ │ ├── shell_launch_background_image.png │ │ └── shell_notification_icon.png │ │ ├── drawable │ │ └── splash_background.xml │ │ ├── layout │ │ ├── error_activity_new.xml │ │ ├── error_console_fragment.xml │ │ ├── error_console_list_item.xml │ │ ├── error_fragment.xml │ │ ├── notification.xml │ │ └── notification_shell_app.xml │ │ ├── mipmap-hdpi │ │ ├── dev_icon.png │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ ├── dev_icon.png │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ ├── dev_icon.png │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ ├── dev_icon.png │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ ├── dev_icon.png │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── debug.keystore ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew └── settings.gradle ├── app.json ├── assets ├── icon.png └── splash.png ├── babel.config.js ├── build-instructions.md ├── deploy_dist.sh ├── etesync.mobileconfig ├── ios ├── EteSyncTests │ ├── EteSyncTests.swift │ └── Info.plist ├── Podfile ├── Podfile.lock ├── etesync.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── etesync.xcscheme ├── etesync.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── etesync │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ ├── AppIcon1024x1024.png │ │ ├── AppIcon20x20@2x.png │ │ ├── AppIcon20x20@3x.png │ │ ├── AppIcon29x29@2x.png │ │ ├── AppIcon29x29@3x.png │ │ ├── AppIcon40x40@2x.png │ │ ├── AppIcon40x40@3x.png │ │ ├── AppIcon60x60@2x.png │ │ ├── AppIcon60x60@3x.png │ │ ├── AppIcon76x76@2x~ipad.png │ │ ├── AppIcon76x76~ipad.png │ │ ├── AppIcon83.5x83.5@2x~ipad.png │ │ └── Contents.json │ ├── Supporting │ ├── EXSDKVersions.plist │ ├── EXShell.json │ ├── EXShell.plist │ ├── Info.plist │ ├── LaunchScreen.xib │ ├── launch_background_image.png │ ├── launch_icon.png │ ├── main.m │ └── sdkVersions.json │ ├── etesync.entitlements │ └── modules │ ├── ContactSerializer.swift │ ├── EteEXCalendar.h │ ├── EteEXCalendar.m │ ├── EtesyncNative.swift │ ├── EtesyncNativeBridge.m │ ├── EtesyncNativeTest.swift │ ├── Utils.swift │ └── etesync-Bridging-Header.h ├── license-gen.js ├── licenses.json ├── package.json ├── shim.js ├── src ├── AboutScreen.tsx ├── AccountWizardScreen.tsx ├── App.tsx ├── CollectionChangelogScreen.tsx ├── CollectionEditScreen.tsx ├── CollectionImportScreen.tsx ├── CollectionItemContact.tsx ├── CollectionItemEvent.tsx ├── CollectionItemScreen.tsx ├── CollectionItemTask.tsx ├── CollectionMemberAddDialog.tsx ├── CollectionMembersScreen.tsx ├── DebugLogsScreen.tsx ├── Drawer.tsx ├── ErrorBoundary.tsx ├── EteSyncNative.ts ├── HomeScreen.tsx ├── InvitationsScreen.tsx ├── JournalEditScreen.tsx ├── JournalEntriesScreen.tsx ├── JournalImportScreen.tsx ├── JournalItemContact.tsx ├── JournalItemEvent.tsx ├── JournalItemHeader.tsx ├── JournalItemSaveScreen.tsx ├── JournalItemScreen.tsx ├── JournalItemTask.tsx ├── JournalMembersScreen.tsx ├── LegacyHomeScreen.tsx ├── LogoutDialog.tsx ├── Permissions.tsx ├── RootNavigator.tsx ├── SettingsGate.tsx ├── SettingsScreen.tsx ├── SettingsScreenLegacy.tsx ├── SyncGate.tsx ├── SyncHandler.tsx ├── components │ ├── EncryptionLoginForm.tsx │ ├── JournalListScreen.tsx │ ├── JournalListScreenEb.tsx │ ├── LoginForm.tsx │ └── WebviewKeygen.tsx ├── constants │ └── index.ts ├── credentials.tsx ├── data │ └── zones.json ├── etesync-helpers.ts ├── helpers.test.tsx ├── helpers.tsx ├── images │ └── icon.png ├── index.tsx ├── logging.ts ├── login │ ├── LoginScreen.tsx │ └── index.tsx ├── pim-types.ts ├── store │ ├── actions.ts │ ├── construct.ts │ ├── index.test.ts │ ├── index.ts │ ├── promise-middleware.ts │ └── reducers.ts ├── sync │ ├── SyncManager.ts │ ├── SyncManagerAddressBook.ts │ ├── SyncManagerBase.ts │ ├── SyncManagerCalendar.ts │ ├── SyncManagerTaskList.ts │ ├── SyncSettings.tsx │ ├── helpers.ts │ ├── index.ts │ └── legacy │ │ ├── SyncManager.ts │ │ ├── SyncManagerAddressBook.ts │ │ ├── SyncManagerBase.ts │ │ ├── SyncManagerCalendar.ts │ │ └── SyncManagerTaskList.ts ├── types │ ├── ical.js.d.ts │ └── redux-persist.d.ts └── widgets │ ├── Alert.tsx │ ├── Checkbox.tsx │ ├── ColorBox.tsx │ ├── ColorPicker.tsx │ ├── ConfirmationDialog.tsx │ ├── Container.tsx │ ├── ErrorDialog.tsx │ ├── ErrorOrLoadingDialog.tsx │ ├── ExternalLink.tsx │ ├── LoadingIndicator.tsx │ ├── Markdown.tsx │ ├── PasswordInput.tsx │ ├── PrettyFingerprint.tsx │ ├── PrettyFingerprintEb.tsx │ ├── Row.tsx │ ├── ScrollView.tsx │ ├── Select.tsx │ ├── Small.tsx │ ├── TextInput.tsx │ ├── Typography.tsx │ └── Wizard.tsx ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "shared-node-browser": true, 4 | "es6": true, 5 | }, 6 | "parser": "@typescript-eslint/parser", 7 | "parserOptions": { 8 | "sourceType": "module", 9 | "ecmaFeatures": { 10 | "jsx": true 11 | } 12 | }, 13 | "settings": { 14 | "react": { 15 | "version": "detect", 16 | }, 17 | }, 18 | "plugins": [ 19 | "@typescript-eslint", 20 | ], 21 | "extends": [ 22 | "eslint:recommended", 23 | "plugin:react/recommended", 24 | "plugin:@typescript-eslint/eslint-recommended", 25 | "plugin:@typescript-eslint/recommended" 26 | ], 27 | "rules": { 28 | "@typescript-eslint/explicit-function-return-type": "off", 29 | "@typescript-eslint/no-use-before-define": "off", 30 | "@typescript-eslint/no-non-null-assertion": "off", 31 | "@typescript-eslint/no-explicit-any": "off", 32 | "@typescript-eslint/member-delimiter-style": ["error", { 33 | "multiline": { 34 | "delimiter": "semi", 35 | "requireLast": true 36 | }, 37 | "singleline": { 38 | "delimiter": "comma", 39 | "requireLast": false 40 | } 41 | }], 42 | "@typescript-eslint/no-unused-vars": ["warn", { 43 | "vars": "all", 44 | "args": "all", 45 | "ignoreRestSiblings": true, 46 | "argsIgnorePattern": "^_", 47 | }], 48 | 49 | "react/display-name": "off", 50 | "react/no-unescaped-entities": "off", 51 | "react/jsx-tag-spacing": ["error", { 52 | "closingSlash": "never", 53 | "beforeSelfClosing": "always", 54 | "afterOpening": "never", 55 | "beforeClosing": "never" 56 | }], 57 | "react/jsx-boolean-value": ["error", "never"], 58 | "react/jsx-curly-spacing": ["error", { "when": "never", "children": true }], 59 | "react/jsx-equals-spacing": ["error", "never"], 60 | "react/jsx-indent-props": ["error", 2], 61 | "react/jsx-curly-brace-presence": ["error", "never"], 62 | "react/jsx-key": ["error", { "checkFragmentShorthand": true }], 63 | "react/jsx-indent": ["error", 2, { checkAttributes: true, indentLogicalExpressions: true }], 64 | "react/void-dom-elements-no-children": ["error"], 65 | "react/no-unknown-property": ["error"], 66 | 67 | "quotes": "off", 68 | "@typescript-eslint/quotes": ["error", "double", { "allowTemplateLiterals": true, "avoidEscape": true }], 69 | "semi": "off", 70 | "@typescript-eslint/semi": ["error", "always", { "omitLastInOneLineBlock": true }], 71 | "comma-dangle": ["error", { 72 | "arrays": "always-multiline", 73 | "objects": "always-multiline", 74 | "imports": "always-multiline", 75 | "exports": "always-multiline", 76 | "functions": "never" 77 | }], 78 | "comma-spacing": ["error"], 79 | "eqeqeq": ["error", "smart"], 80 | "indent": "off", 81 | "@typescript-eslint/indent": ["error", 2, { 82 | "SwitchCase": 1, 83 | }], 84 | "no-multi-spaces": "error", 85 | "object-curly-spacing": ["error", "always"], 86 | "arrow-parens": "error", 87 | "arrow-spacing": "error", 88 | "key-spacing": "error", 89 | "keyword-spacing": "error", 90 | "func-call-spacing": "off", 91 | "@typescript-eslint/func-call-spacing": ["error"], 92 | "space-before-function-paren": ["error", { 93 | "anonymous": "always", 94 | "named": "never", 95 | "asyncArrow": "always" 96 | }], 97 | "space-in-parens": ["error", "never"], 98 | "space-before-blocks": "error", 99 | "curly": ["error", "all"], 100 | "space-infix-ops": "error", 101 | "consistent-return": "error", 102 | "jsx-quotes": ["error"], 103 | "array-bracket-spacing": "error", 104 | "brace-style": "off", 105 | "@typescript-eslint/brace-style": [ 106 | "error", 107 | "1tbs", 108 | { allowSingleLine: true }, 109 | ], 110 | "no-useless-constructor": "off", 111 | "@typescript-eslint/no-useless-constructor": "warn", 112 | } 113 | }; 114 | -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "bcbfadc7151fde99a5ae3871cdabbd22f007ea6cbaee2be3b3d87dc27101c53a": true, 3 | "af0ce96885147c05d7b5bf4162cce9dc050bdbf338b0bae9a3ec86b4f9a833a9": true, 4 | "baddda505ed7aa3bd17ad9541c791e7834f15e9433bb3b044683427021e414e4": true 5 | } 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: etesync 2 | custom: https://www.etesync.com/contribute/#donate 3 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Tom Hacohen 2 | -------------------------------------------------------------------------------- /App.ts: -------------------------------------------------------------------------------- 1 | import './shim'; 2 | import Index from './src/index'; 3 | export default Index; 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

EteSync - Secure Data Sync

4 |

5 | 6 | Secure, end-to-end encrypted, and privacy respecting sync for your contacts, calendars and tasks (iOS client). 7 | 8 | ![GitHub tag](https://img.shields.io/github/tag/etesync/ios.svg) 9 | [![Chat with us](https://img.shields.io/badge/chat-IRC%20|%20Matrix%20|%20Web-blue.svg)](https://www.etebase.com/community-chat/) 10 | 11 | # Overview 12 | 13 | Please see the [EteSync website](https://www.etesync.com) for more information. 14 | 15 | EteSync is licensed under the [GPLv3 License](LICENSE). 16 | 17 | # Setup 18 | 19 | For setup instructions please take a look at the [user guide](https://www.etesync.com/user-guide/ios/). 20 | 21 | # Thanks 22 | 23 |

EteSync iOS is made possible with financial support from NLnet Foundation, courtesy of NGI0 Discovery and the European Commission DG 28 | CNECT's Next Generation Internet 29 | programme.

30 | -------------------------------------------------------------------------------- /android/app/expo.gradle: -------------------------------------------------------------------------------- 1 | // Gradle script for detached apps. 2 | 3 | import org.apache.tools.ant.taskdefs.condition.Os 4 | 5 | void runBefore(String dependentTaskName, Task task) { 6 | Task dependentTask = tasks.findByPath(dependentTaskName); 7 | if (dependentTask != null) { 8 | dependentTask.dependsOn task 9 | } 10 | } 11 | 12 | afterEvaluate { 13 | def expoRoot = file("../../") 14 | def inputExcludes = ["android/**", "ios/**"] 15 | 16 | task exponentPrebuildStep(type: Exec) { 17 | workingDir expoRoot 18 | if (Os.isFamily(Os.FAMILY_WINDOWS)) { 19 | commandLine "cmd", "/c", ".\\node_modules\\expokit\\detach-scripts\\run-exp.bat" 20 | } else { 21 | commandLine "./node_modules/expokit/detach-scripts/run-exp.sh", "prepare-detached-build", "--platform", "android", expoRoot 22 | } 23 | } 24 | runBefore("preBuild", exponentPrebuildStep) 25 | 26 | // Based on https://github.com/facebook/react-native/blob/master/react.gradle 27 | 28 | android.applicationVariants.each { variant -> 29 | def folderName = variant.name 30 | def targetName = folderName.capitalize() 31 | 32 | def assetsDir = file("$buildDir/intermediates/merged_assets/${folderName}/out") 33 | 34 | // Bundle task name for variant 35 | def bundleExpoAssetsTaskName = "bundle${targetName}ExpoAssets" 36 | 37 | def currentBundleTask = tasks.create( 38 | name: bundleExpoAssetsTaskName, 39 | type: Exec) { 40 | description = "Expo bundle assets for ${targetName}." 41 | 42 | // Create dirs if they are not there (e.g. the "clean" task just ran) 43 | doFirst { 44 | assetsDir.mkdirs() 45 | } 46 | 47 | // Set up inputs and outputs so gradle can cache the result 48 | inputs.files fileTree(dir: expoRoot, excludes: inputExcludes) 49 | outputs.dir assetsDir 50 | 51 | // Set up the call to exp 52 | workingDir expoRoot 53 | 54 | if (Os.isFamily(Os.FAMILY_WINDOWS)) { 55 | commandLine("cmd", "/c", ".\\node_modules\\expokit\\detach-scripts\\run-exp.bat", "bundle-assets", expoRoot, "--platform", "android", "--dest", assetsDir) 56 | } else { 57 | commandLine("./node_modules/expokit/detach-scripts/run-exp.sh", "bundle-assets", expoRoot, "--platform", "android", "--dest", assetsDir) 58 | } 59 | 60 | enabled targetName.toLowerCase().contains("release") || targetName.toLowerCase().contains("prod") 61 | } 62 | 63 | currentBundleTask.dependsOn("merge${targetName}Resources") 64 | currentBundleTask.dependsOn("merge${targetName}Assets") 65 | 66 | runBefore("process${targetName}Resources", currentBundleTask) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /android/app/src/main/assets/kernel-manifest.json: -------------------------------------------------------------------------------- 1 | {"ios":{"supportsTablet":true,"bundleIdentifier":"host.exp.exponent","publishBundlePath":"../ios/Exponent/Supporting/kernel.ios.bundle"},"icon":"https://s3.amazonaws.com/exp-brand-assets/ExponentEmptyManifest_192.png","name":"expo-home","slug":"home","extra":{"amplitudeApiKey":"081e5ec53f869b440b225d5e40ec73f9"},"kernel":{"iosManifestPath":"../ios/Exponent/Supporting/kernel-manifest.json","androidManifestPath":"../android/app/src/main/assets/kernel-manifest.json"},"scheme":"exp","android":{"package":"host.exp.exponent","publishBundlePath":"../android/app/src/main/assets/kernel.android.bundle"},"iconUrl":"https://s3.amazonaws.com/exp-brand-assets/ExponentEmptyManifest_192.png","locales":{},"privacy":"unlisted","updates":{"checkAutomatically":"ON_LOAD","fallbackToCacheTimeout":0},"version":"36.0.0","isKernel":true,"platforms":["ios","android"],"sdkVersion":"UNVERSIONED","description":"The Expo client app","orientation":"portrait","dependencies":["@expo/react-native-action-sheet","@expo/react-native-touchable-native-feedback-safe","@react-native-community/netinfo","@react-navigation/web","apollo-boost","apollo-cache-inmemory","dedent","es6-error","expo","expo-analytics-amplitude","expo-asset","expo-barcode-scanner","expo-blur","expo-camera","expo-constants","expo-font","expo-linear-gradient","expo-location","expo-permissions","expo-task-manager","expo-web-browser","graphql","graphql-tag","immutable","lodash","prop-types","querystring","react","react-apollo","react-native","react-native-appearance","react-native-fade-in-image","react-native-gesture-handler","react-native-infinite-scroll-view","react-native-maps","react-navigation","react-navigation-material-bottom-tabs","react-navigation-stack","react-navigation-tabs","react-redux","redux","redux-thunk","semver","sha1","url"],"packagerOpts":{"config":"metro.config.js"},"primaryColor":"#cccccc","userInterfaceStyle":"automatic","id":"@exponent/home","revisionId":"36.0.0-r.SygW7r7mpr","publishedTime":"2019-12-02T23:58:17.039Z","commitTime":"2019-12-02T23:58:17.148Z","bundleUrl":"https://exp.host/@exponent/home/bundle","releaseChannel":"default","hostUri":"exp.host/@exponent/home"} -------------------------------------------------------------------------------- /android/app/src/main/java/host/exp/exponent/MainActivity.java: -------------------------------------------------------------------------------- 1 | package host.exp.exponent; 2 | 3 | import android.os.Bundle; 4 | 5 | import com.facebook.react.ReactPackage; 6 | 7 | import org.unimodules.core.interfaces.Package; 8 | 9 | import java.util.List; 10 | 11 | import host.exp.exponent.experience.DetachActivity; 12 | import host.exp.exponent.generated.DetachBuildConstants; 13 | 14 | public class MainActivity extends DetachActivity { 15 | 16 | @Override 17 | public String publishedUrl() { 18 | return "exp://exp.host/@etesync/etesync-ios"; 19 | } 20 | 21 | @Override 22 | public String developmentUrl() { 23 | return DetachBuildConstants.DEVELOPMENT_URL; 24 | } 25 | 26 | @Override 27 | public List reactPackages() { 28 | return ((MainApplication) getApplication()).getPackages(); 29 | } 30 | 31 | @Override 32 | public List expoPackages() { 33 | return ((MainApplication) getApplication()).getExpoPackages(); 34 | } 35 | 36 | @Override 37 | public boolean isDebug() { 38 | return BuildConfig.DEBUG; 39 | } 40 | 41 | @Override 42 | public Bundle initialProps(Bundle expBundle) { 43 | // Add extra initialProps here 44 | return expBundle; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /android/app/src/main/java/host/exp/exponent/MainApplication.java: -------------------------------------------------------------------------------- 1 | package host.exp.exponent; 2 | 3 | import com.facebook.react.ReactPackage; 4 | 5 | import org.unimodules.core.interfaces.Package; 6 | 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | import expo.loaders.provider.interfaces.AppLoaderPackagesProviderInterface; 11 | import host.exp.exponent.generated.BasePackageList; 12 | import okhttp3.OkHttpClient; 13 | 14 | // Needed for `react-native link` 15 | // import com.facebook.react.ReactApplication; 16 | import com.RNRSA.RNRSAPackage; 17 | 18 | public class MainApplication extends ExpoApplication implements AppLoaderPackagesProviderInterface { 19 | 20 | @Override 21 | public boolean isDebug() { 22 | return BuildConfig.DEBUG; 23 | } 24 | 25 | // Needed for `react-native link` 26 | public List getPackages() { 27 | return Arrays.asList( 28 | // Add your own packages here! 29 | // TODO: add native modules! 30 | 31 | // Needed for `react-native link` 32 | // new MainReactPackage(), 33 | new RNRSAPackage() 34 | ); 35 | } 36 | 37 | public List getExpoPackages() { 38 | return new BasePackageList().getPackageList(); 39 | } 40 | 41 | @Override 42 | public String gcmSenderId() { 43 | return getString(R.string.gcm_defaultSenderId); 44 | } 45 | 46 | public static OkHttpClient.Builder okHttpClientBuilder(OkHttpClient.Builder builder) { 47 | // Customize/override OkHttp client here 48 | return builder; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /android/app/src/main/java/host/exp/exponent/generated/AppConstants.java: -------------------------------------------------------------------------------- 1 | package host.exp.exponent.generated; 2 | 3 | import com.facebook.common.internal.DoNotStrip; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import host.exp.exponent.BuildConfig; 9 | import host.exp.exponent.Constants; 10 | 11 | @DoNotStrip 12 | public class AppConstants { 13 | 14 | public static final String VERSION_NAME = "1.1.3"; 15 | public static String INITIAL_URL = "exp://exp.host/@etesync/etesync-ios"; 16 | public static final String SHELL_APP_SCHEME = "exp2904f03229f24a3ca9b3fcd8485120ca"; 17 | public static final String RELEASE_CHANNEL = "default"; 18 | public static boolean SHOW_LOADING_VIEW_IN_SHELL_APP = true; 19 | public static boolean ARE_REMOTE_UPDATES_ENABLED = true; 20 | public static final List EMBEDDED_RESPONSES; 21 | public static boolean FCM_ENABLED = false; 22 | 23 | static { 24 | List embeddedResponses = new ArrayList<>(); 25 | 26 | 27 | // ADD EMBEDDED RESPONSES HERE 28 | // START EMBEDDED RESPONSES 29 | embeddedResponses.add(new Constants.EmbeddedResponse("https://expo.etesync.com/release/5/android-index.json", "assets://shell-app-manifest.json", "application/json")); 30 | embeddedResponses.add(new Constants.EmbeddedResponse("https://expo.etesync.com/release/5/bundles/android-d9c315522a5c2e60cdc6c397465e7e31.js", "assets://shell-app.bundle", "application/javascript")); 31 | // END EMBEDDED RESPONSES 32 | EMBEDDED_RESPONSES = embeddedResponses; 33 | } 34 | 35 | // Called from expoview/Constants 36 | public static Constants.ExpoViewAppConstants get() { 37 | Constants.ExpoViewAppConstants constants = new Constants.ExpoViewAppConstants(); 38 | constants.VERSION_NAME = VERSION_NAME; 39 | constants.INITIAL_URL = INITIAL_URL; 40 | constants.SHELL_APP_SCHEME = SHELL_APP_SCHEME; 41 | constants.RELEASE_CHANNEL = RELEASE_CHANNEL; 42 | constants.SHOW_LOADING_VIEW_IN_SHELL_APP = SHOW_LOADING_VIEW_IN_SHELL_APP; 43 | constants.ARE_REMOTE_UPDATES_ENABLED = ARE_REMOTE_UPDATES_ENABLED; 44 | constants.EMBEDDED_RESPONSES = EMBEDDED_RESPONSES; 45 | constants.ANDROID_VERSION_CODE = BuildConfig.VERSION_CODE; 46 | constants.FCM_ENABLED = FCM_ENABLED; 47 | return constants; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /android/app/src/main/java/host/exp/exponent/generated/DetachBuildConstants.java: -------------------------------------------------------------------------------- 1 | package host.exp.exponent.generated; 2 | 3 | // This file is auto-generated. Please don't rename! 4 | public class DetachBuildConstants { 5 | 6 | public static final String DEVELOPMENT_URL = ""; 7 | 8 | } 9 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_arrow_back_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-hdpi/ic_arrow_back_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_home_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-hdpi/ic_home_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_logo_white_32dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-hdpi/ic_logo_white_32dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_refresh_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-hdpi/ic_refresh_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_share_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-hdpi/ic_share_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_arrow_back_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-mdpi/ic_arrow_back_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_home_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-mdpi/ic_home_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_logo_white_32dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-mdpi/ic_logo_white_32dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_refresh_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-mdpi/ic_refresh_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_share_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-mdpi/ic_share_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_arrow_back_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xhdpi/ic_arrow_back_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_home_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xhdpi/ic_home_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_logo_white_32dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xhdpi/ic_logo_white_32dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_refresh_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xhdpi/ic_refresh_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_share_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xhdpi/ic_share_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_home_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxhdpi/ic_home_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_logo_white_32dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxhdpi/ic_logo_white_32dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_refresh_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxhdpi/ic_refresh_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_share_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxhdpi/ic_share_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/big_logo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxxhdpi/big_logo_dark.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/big_logo_dark_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxxhdpi/big_logo_dark_filled.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/big_logo_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxxhdpi/big_logo_filled.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/big_logo_new_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxxhdpi/big_logo_new_filled.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_home_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxxhdpi/ic_home_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_logo_white_32dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxxhdpi/ic_logo_white_32dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_refresh_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxxhdpi/ic_refresh_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_share_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxxhdpi/ic_share_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/pin_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxxhdpi/pin_white.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/pin_white_fade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxxhdpi/pin_white_fade.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/shell_launch_background_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxxhdpi/shell_launch_background_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/shell_notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/android/app/src/main/res/drawable-xxxhdpi/shell_notification_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/splash_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/error_activity_new.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/error_console_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 24 | 25 | 34 | 35 | 44 | 45 | 46 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/error_console_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 19 | 20 | 25 | 26 | 31 | 32 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/error_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 21 | 22 | 31 | 32 | 44 | 45 | 56 | 57 | 66 | 67 | 76 | 77 | 78 | 79 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/notification.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | {(logs) ? logs : "No logs found"} 29 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/EteSyncNative.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { NativeModules } from "react-native"; 5 | import * as Contacts from "expo-contacts"; 6 | import { NativeContact, NativeEvent, NativeTask } from "./sync/helpers"; 7 | 8 | export type HashesForItem = [string, string, string | undefined]; 9 | 10 | export type HashDictionary = { [key: string]: HashesForItem }; 11 | 12 | export enum BatchAction { 13 | Add = 1, 14 | Change = 2, 15 | Delete = 3 16 | } 17 | 18 | interface EteSyncNativeModule { 19 | hashEvent(eventId: string): Promise; 20 | calculateHashesForEvents(calendarId: string, from: number, to: number): Promise; 21 | processEventsChanges(containerId: string, events: ([BatchAction, NativeEvent])[]): Promise; 22 | hashReminder(eventId: string): Promise; 23 | calculateHashesForReminders(calendarId: string): Promise; 24 | processRemindersChanges(containerId: string, reminders: ([BatchAction, NativeTask])[]): Promise; 25 | hashContact(contactId: string): Promise; 26 | calculateHashesForContacts(containerId: string): Promise; 27 | deleteContactGroupAndMembers(groupId: string): Promise; 28 | getContainers(): Promise<(Contacts.Container & { default: boolean })[]>; 29 | processContactsChanges(containerId: string, groupId: string | null, contacts: ([BatchAction, NativeContact])[]): Promise; 30 | 31 | beginBackgroundTask(name: string): Promise; 32 | endBackgroundTask(taskId: number): void; 33 | 34 | playground(param: {}): void; 35 | } 36 | 37 | const EteSyncNative = NativeModules.EteSyncNative as EteSyncNativeModule; 38 | 39 | export function calculateHashesForEvents(calendarId: string, from: Date, to: Date): Promise { 40 | return EteSyncNative.calculateHashesForEvents(calendarId, from.getTime() / 1000, to.getTime() / 1000); 41 | } 42 | 43 | export const hashEvent = EteSyncNative.hashEvent; 44 | 45 | export function calculateHashesForReminders(calendarId: string): Promise { 46 | return EteSyncNative.calculateHashesForReminders(calendarId); 47 | } 48 | export const processEventsChanges = EteSyncNative.processEventsChanges; 49 | 50 | export const hashReminder = EteSyncNative.hashReminder; 51 | 52 | export function calculateHashesForContacts(containerId: string): Promise { 53 | return EteSyncNative.calculateHashesForContacts(containerId); 54 | } 55 | export const processRemindersChanges = EteSyncNative.processRemindersChanges; 56 | 57 | export const hashContact = EteSyncNative.hashContact; 58 | export const deleteContactGroupAndMembers = EteSyncNative.deleteContactGroupAndMembers; 59 | export const getContainers = EteSyncNative.getContainers; 60 | export const processContactsChanges = EteSyncNative.processContactsChanges; 61 | 62 | export const { beginBackgroundTask, endBackgroundTask } = EteSyncNative; 63 | 64 | 65 | export const playground = EteSyncNative.playground; 66 | -------------------------------------------------------------------------------- /src/HomeScreen.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { useDispatch, useSelector } from "react-redux"; 6 | import { Appbar } from "react-native-paper"; 7 | import { useNavigation } from "@react-navigation/native"; 8 | 9 | import { SyncManager } from "./sync/SyncManager"; 10 | 11 | import JournalListScreen from "./components/JournalListScreenEb"; 12 | import { usePermissions } from "./Permissions"; 13 | 14 | import { StoreState } from "./store"; 15 | import { performSync } from "./store/actions"; 16 | 17 | import { useCredentials } from "./credentials"; 18 | import { registerSyncTask } from "./sync/SyncManager"; 19 | 20 | 21 | export default React.memo(function HomeScreen() { 22 | const etebase = useCredentials()!; 23 | const dispatch = useDispatch(); 24 | const navigation = useNavigation(); 25 | const syncCount = useSelector((state: StoreState) => state.syncCount); 26 | const permissionsStatus = usePermissions(); 27 | 28 | React.useEffect(() => { 29 | if (etebase && !permissionsStatus) { 30 | registerSyncTask(etebase.user.username); 31 | } 32 | }, [etebase, !permissionsStatus]); 33 | 34 | function refresh() { 35 | const syncManager = SyncManager.getManager(etebase); 36 | dispatch(performSync(syncManager.sync())); 37 | } 38 | 39 | navigation.setOptions({ 40 | headerRight: () => ( 41 | 0} onPress={refresh} /> 42 | ), 43 | }); 44 | 45 | if (permissionsStatus) { 46 | return permissionsStatus; 47 | } 48 | 49 | return ( 50 | 51 | ); 52 | }); 53 | -------------------------------------------------------------------------------- /src/InvitationsScreen.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import * as Etebase from "etebase"; 6 | import { List, Paragraph, IconButton } from "react-native-paper"; 7 | 8 | import { useSyncGateEb } from "./SyncGate"; 9 | import { useCredentials } from "./credentials"; 10 | 11 | import ScrollView from "./widgets/ScrollView"; 12 | import Container from "./widgets/Container"; 13 | import LoadingIndicator from "./widgets/LoadingIndicator"; 14 | import ConfirmationDialog from "./widgets/ConfirmationDialog"; 15 | import PrettyFingerprint from "./widgets/PrettyFingerprintEb"; 16 | 17 | 18 | async function loadInvitations(etebase: Etebase.Account) { 19 | const ret: Etebase.SignedInvitation[] = []; 20 | const invitationManager = etebase.getInvitationManager(); 21 | 22 | let iterator: string | null = null; 23 | let done = false; 24 | while (!done) { 25 | const invitations = await invitationManager.listIncoming({ iterator, limit: 30 }); 26 | iterator = invitations.iterator as string; 27 | done = invitations.done; 28 | 29 | ret.push(...invitations.data); 30 | } 31 | 32 | return ret; 33 | } 34 | 35 | export default function InvitationsScreen() { 36 | const [invitations, setInvitations] = React.useState(); 37 | const [chosenInvitation, setChosenInvitation] = React.useState(); 38 | const etebase = useCredentials()!; 39 | const syncGate = useSyncGateEb(); 40 | 41 | React.useEffect(() => { 42 | loadInvitations(etebase).then(setInvitations); 43 | }, [etebase]); 44 | 45 | function removeInvitation(invite: Etebase.SignedInvitation) { 46 | setInvitations(invitations?.filter((x) => x.uid !== invite.uid)); 47 | } 48 | 49 | async function reject(invite: Etebase.SignedInvitation) { 50 | const invitationManager = etebase.getInvitationManager(); 51 | await invitationManager.reject(invite); 52 | removeInvitation(invite); 53 | } 54 | 55 | async function accept(invite: Etebase.SignedInvitation) { 56 | const invitationManager = etebase.getInvitationManager(); 57 | await invitationManager.accept(invite); 58 | setChosenInvitation(undefined); 59 | removeInvitation(invite); 60 | } 61 | 62 | if (syncGate) { 63 | return syncGate; 64 | } 65 | 66 | return ( 67 | 68 | 69 | {invitations ? 70 | <> 71 | {(invitations.length > 0 ? 72 | invitations.map((invite) => ( 73 | ( 77 | <> 78 | { reject(invite) }} /> 79 | { setChosenInvitation(invite) }} /> 80 | 81 | )} 82 | /> 83 | )) 84 | : 85 | 88 | )} 89 | 90 | : 91 | 92 | } 93 | 94 | {chosenInvitation && ( 95 | accept(chosenInvitation)} 100 | onCancel={() => setChosenInvitation(undefined)} 101 | > 102 | 103 | Please verify the inviter's security fingerprint to ensure the invitation is secure: 104 | 105 | 106 | 107 | )} 108 | 109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /src/JournalItemContact.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import moment from "moment"; 6 | import * as EteSync from "etesync"; 7 | 8 | import { Clipboard, Linking } from "react-native"; 9 | import { Text, List, Divider } from "react-native-paper"; 10 | 11 | import { SyncInfoItem } from "./store"; 12 | 13 | import Container from "./widgets/Container"; 14 | 15 | import { ContactType } from "./pim-types"; 16 | 17 | import JournalItemHeader from "./JournalItemHeader"; 18 | 19 | interface PropsType { 20 | collection: EteSync.CollectionInfo; 21 | entry: SyncInfoItem; 22 | } 23 | 24 | export default React.memo(function JournalItemContact(props: PropsType) { 25 | const entry = props.entry; 26 | const contact = ContactType.parse(entry.content); 27 | 28 | const revProp = contact.comp.getFirstProperty("rev"); 29 | const lastModified = (revProp) ? moment(revProp.getFirstValue().toJSDate()).format("LLLL") : undefined; 30 | 31 | const lists = []; 32 | 33 | function getAllType( 34 | propName: string, 35 | leftIcon: string, 36 | valueToHref?: (value: string, type: string) => string, 37 | primaryTransform?: (value: string, type: string) => string, 38 | secondaryTransform?: (value: string, type: string) => string) { 39 | 40 | return contact.comp.getAllProperties(propName).map((prop, idx) => { 41 | const type = prop.toJSON()[1].type; 42 | const values = prop.getValues().map((val) => { 43 | const primaryText = primaryTransform ? primaryTransform(val, type) : val; 44 | 45 | const href = valueToHref?.(val, type); 46 | const onPress = (href && Linking.canOpenURL(href)) ? (() => { Linking.openURL(href) }) : undefined; 47 | 48 | return ( 49 | Clipboard.setString(primaryText)} 54 | left={(props) => } 55 | description={secondaryTransform ? secondaryTransform(val, type) : type} 56 | /> 57 | ); 58 | }); 59 | return values; 60 | }); 61 | } 62 | 63 | lists.push(getAllType( 64 | "tel", 65 | "phone", 66 | (x) => ("tel:" + x) 67 | )); 68 | 69 | lists.push(getAllType( 70 | "email", 71 | "email", 72 | (x) => ("mailto:" + x) 73 | )); 74 | 75 | lists.push(getAllType( 76 | "impp", 77 | "chat", 78 | (x) => x, 79 | (x) => (x.substring(x.indexOf(":") + 1)), 80 | (x) => (x.substring(0, x.indexOf(":"))) 81 | )); 82 | 83 | lists.push(getAllType( 84 | "adr", 85 | "home" 86 | )); 87 | 88 | lists.push(getAllType( 89 | "bday", 90 | "calendar", 91 | undefined, 92 | ((x: any) => (x.toJSDate) ? moment(x.toJSDate()).format("dddd, LL") : x), 93 | () => "Birthday" 94 | )); 95 | 96 | lists.push(getAllType( 97 | "anniversary", 98 | "calendar", 99 | undefined, 100 | ((x: any) => (x.toJSDate) ? moment(x.toJSDate()).format("dddd, LL") : x), 101 | () => "Anniversary" 102 | )); 103 | 104 | const skips = ["tel", "email", "impp", "adr", "bday", "anniversary", "rev", 105 | "prodid", "uid", "fn", "n", "version", "photo"]; 106 | const theRest = contact.comp.getAllProperties().filter((prop) => ( 107 | skips.indexOf(prop.name) === -1 108 | )).map((prop, idx) => { 109 | const values = prop.getValues().map((_val) => { 110 | const val = (_val instanceof String) ? _val : _val.toString(); 111 | return ( 112 | Clipboard.setString(val)} 116 | description={prop.name} 117 | /> 118 | ); 119 | }); 120 | return values; 121 | }); 122 | 123 | function listIfNotEmpty(items: JSX.Element[][]) { 124 | if (items.length > 0) { 125 | return ( 126 | 127 | {items} 128 | 129 | 130 | ); 131 | } else { 132 | return undefined; 133 | } 134 | } 135 | 136 | return ( 137 | <> 138 | 139 | {lastModified && ( 140 | Modified: {lastModified} 141 | )} 142 | 143 | 144 | {lists.map((list, idx) => ( 145 | 146 | {listIfNotEmpty(list)} 147 | 148 | ))} 149 | {theRest} 150 | 151 | 152 | ); 153 | }); 154 | -------------------------------------------------------------------------------- /src/JournalItemEvent.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import * as EteSync from "etesync"; 6 | 7 | import Color from "color"; 8 | 9 | import { Text } from "react-native-paper"; 10 | 11 | import { SyncInfoItem } from "./store"; 12 | 13 | import Container from "./widgets/Container"; 14 | import Small from "./widgets/Small"; 15 | 16 | import { EventType } from "./pim-types"; 17 | import { formatDateRange, formatOurTimezoneOffset, colorIntToHtml } from "./helpers"; 18 | 19 | import JournalItemHeader from "./JournalItemHeader"; 20 | 21 | interface PropsType { 22 | collection: EteSync.CollectionInfo; 23 | entry: SyncInfoItem; 24 | } 25 | 26 | export default React.memo(function JournalItemEvent(props: PropsType) { 27 | const entry = props.entry; 28 | const event = EventType.parse(entry.content); 29 | 30 | const timezone = event.timezone; 31 | 32 | const backgroundColor = colorIntToHtml(props.collection.color); 33 | const foregroundColor = Color(backgroundColor).isLight() ? "black" : "white"; 34 | 35 | return ( 36 | <> 37 | 38 | {formatDateRange(event.startDate, event.endDate)} {timezone && ({formatOurTimezoneOffset()})} 39 | {event.location} 40 | 41 | 42 | {event.description} 43 | {(event.attendees.length > 0) && ( 44 | Attendees: {event.attendees.map((x) => (x.getFirstValue())).join(", ")})} 45 | 46 | 47 | ); 48 | }); 49 | -------------------------------------------------------------------------------- /src/JournalItemHeader.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | 6 | import { useTheme } from "react-native-paper"; 7 | 8 | import Container from "./widgets/Container"; 9 | import { Title } from "./widgets/Typography"; 10 | 11 | interface HeaderPropsType { 12 | title: string; 13 | foregroundColor?: string; 14 | backgroundColor?: string; 15 | } 16 | 17 | export default function JournalItemHeader(props: React.PropsWithChildren) { 18 | const theme = useTheme(); 19 | 20 | return ( 21 | 22 | {props.title} 23 | {props.children} 24 | 25 | ); 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/JournalItemScreen.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | 6 | import { useSelector } from "react-redux"; 7 | import { StyleSheet } from "react-native"; 8 | import { Text, FAB, Appbar } from "react-native-paper"; 9 | import { RouteProp, useNavigation } from "@react-navigation/native"; 10 | 11 | import { useSyncGate } from "./SyncGate"; 12 | import { StoreState } from "./store"; 13 | 14 | import ScrollView from "./widgets/ScrollView"; 15 | import Container from "./widgets/Container"; 16 | 17 | import JournalItemContact from "./JournalItemContact"; 18 | import JournalItemEvent from "./JournalItemEvent"; 19 | import JournalItemTask from "./JournalItemTask"; 20 | 21 | type RootStackParamList = { 22 | JournalItemScreen: { 23 | journalUid: string; 24 | entryUid: string; 25 | }; 26 | }; 27 | 28 | interface PropsType { 29 | route: RouteProp; 30 | } 31 | 32 | export default function JournalItemScreen(props: PropsType) { 33 | const [showRaw, setShowRaw] = React.useState(false); 34 | const navigation = useNavigation(); 35 | const syncGate = useSyncGate(); 36 | const syncInfoCollections = useSelector((state: StoreState) => state.cache.syncInfoCollection); 37 | const syncInfoEntries = useSelector((state: StoreState) => state.cache.syncInfoItem); 38 | 39 | if (syncGate) { 40 | return syncGate; 41 | } 42 | 43 | const { journalUid, entryUid } = props.route.params; 44 | const collection = syncInfoCollections.get(journalUid)!; 45 | const entries = syncInfoEntries.get(journalUid)!; 46 | 47 | const entry = entries.get(entryUid)!; 48 | 49 | let content; 50 | let fabContentIcon = ""; 51 | switch (collection.type) { 52 | case "ADDRESS_BOOK": 53 | content = ; 54 | fabContentIcon = "account-card-details"; 55 | break; 56 | case "CALENDAR": 57 | content = ; 58 | fabContentIcon = "calendar"; 59 | break; 60 | case "TASKS": 61 | content = ; 62 | fabContentIcon = "format-list-checkbox"; 63 | break; 64 | } 65 | 66 | navigation.setOptions({ 67 | headerRight: () => ( 68 | { navigation.navigate("JournalItemSave", { journalUid, entryUid }) }} /> 69 | ), 70 | }); 71 | 72 | return ( 73 | <> 74 | 75 | {showRaw ? ( 76 | 77 | {entry.content} 78 | 79 | ) : ( 80 | content 81 | )} 82 | 83 | setShowRaw(!showRaw)} 89 | /> 90 | 91 | ); 92 | } 93 | 94 | const styles = StyleSheet.create({ 95 | fab: { 96 | position: "absolute", 97 | margin: 16, 98 | right: 0, 99 | bottom: 0, 100 | }, 101 | }); 102 | -------------------------------------------------------------------------------- /src/JournalItemTask.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import * as EteSync from "etesync"; 6 | 7 | import Color from "color"; 8 | 9 | import { Text } from "react-native-paper"; 10 | 11 | import { SyncInfoItem } from "./store"; 12 | 13 | import Container from "./widgets/Container"; 14 | import Small from "./widgets/Small"; 15 | 16 | import { TaskType } from "./pim-types"; 17 | import { formatDate, formatOurTimezoneOffset, colorIntToHtml } from "./helpers"; 18 | 19 | import JournalItemHeader from "./JournalItemHeader"; 20 | 21 | interface PropsType { 22 | collection: EteSync.CollectionInfo; 23 | entry: SyncInfoItem; 24 | } 25 | 26 | export default React.memo(function JournalItemTask(props: PropsType) { 27 | const entry = props.entry; 28 | const task = TaskType.parse(entry.content); 29 | 30 | const timezone = task.timezone; 31 | 32 | const backgroundColor = colorIntToHtml(props.collection.color); 33 | const foregroundColor = Color(backgroundColor).isLight() ? "black" : "white"; 34 | 35 | return ( 36 | <> 37 | 38 | {task.startDate && 39 | Start: {formatDate(task.startDate)} {timezone && ({formatOurTimezoneOffset()})} 40 | } 41 | {task.dueDate && 42 | Due: {formatDate(task.dueDate)} {timezone && ({formatOurTimezoneOffset()})} 43 | } 44 | {task.location} 45 | 46 | 47 | {task.description} 48 | {(task.attendees.length > 0) && ( 49 | Attendees: {task.attendees.map((x) => (x.getFirstValue())).join(", ")})} 50 | 51 | 52 | ); 53 | }); 54 | -------------------------------------------------------------------------------- /src/LegacyHomeScreen.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { useDispatch, useSelector } from "react-redux"; 6 | import { Appbar } from "react-native-paper"; 7 | import { useNavigation } from "@react-navigation/native"; 8 | 9 | import { SyncManager } from "./sync/SyncManager"; 10 | 11 | import JournalListScreen from "./components/JournalListScreen"; 12 | import { usePermissions } from "./Permissions"; 13 | 14 | import { StoreState } from "./store"; 15 | import { performSync } from "./store/actions"; 16 | 17 | import { useCredentials } from "./login"; 18 | import { useSyncGate } from "./SyncGate"; 19 | import { registerSyncTask } from "./sync/SyncManager"; 20 | 21 | 22 | export default React.memo(function HomeScreen() { 23 | const etesync = useCredentials()!; 24 | const dispatch = useDispatch(); 25 | const SyncGate = useSyncGate(); 26 | const navigation = useNavigation(); 27 | const syncCount = useSelector((state: StoreState) => state.syncCount); 28 | const permissionsStatus = usePermissions(); 29 | 30 | React.useEffect(() => { 31 | if (etesync && !permissionsStatus) { 32 | registerSyncTask(etesync.credentials.email); 33 | } 34 | }, [etesync, !permissionsStatus]); 35 | 36 | function refresh() { 37 | const syncManager = SyncManager.getManagerLegacy(etesync); 38 | dispatch(performSync(syncManager.sync())); 39 | } 40 | 41 | navigation.setOptions({ 42 | headerRight: () => ( 43 | 0} onPress={refresh} /> 44 | ), 45 | }); 46 | 47 | if (permissionsStatus) { 48 | return permissionsStatus; 49 | } 50 | 51 | if (SyncGate) { 52 | return SyncGate; 53 | } 54 | 55 | return ( 56 | 57 | ); 58 | }); 59 | -------------------------------------------------------------------------------- /src/Permissions.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { useDispatch, useSelector } from "react-redux"; 6 | import { Button, Paragraph } from "react-native-paper"; 7 | 8 | import * as Permissions from "expo-permissions"; 9 | 10 | import { Title } from "./widgets/Typography"; 11 | import LoadingIndicator from "./widgets/LoadingIndicator"; 12 | 13 | import { StoreState } from "./store"; 14 | import { setPermission } from "./store/actions"; 15 | 16 | import { logger } from "./logging"; 17 | 18 | const wantedPermissions: Permissions.PermissionType[] = [Permissions.CALENDAR, Permissions.REMINDERS, Permissions.CONTACTS, Permissions.USER_FACING_NOTIFICATIONS]; 19 | 20 | export function AskForPermissions() { 21 | const dispatch = useDispatch(); 22 | const permissions = useSelector((state: StoreState) => state.permissions); 23 | const alreadyAsked = wantedPermissions.length === permissions.size; 24 | 25 | return ( 26 | <> 27 | Permissions 28 | {(alreadyAsked) ? 29 | 30 | EteSync has already asked for the permissions before, so they can now only be changed from the device's Settings app. 31 | 32 | : 33 | EteSync requires access to your contacts, calendars and reminders in order to be able save them to your device. You can either give EteSync access now or do it later from the device Settings. 34 | } 35 | 46 | 47 | ); 48 | } 49 | 50 | 51 | export function usePermissions() { 52 | const dispatch = useDispatch(); 53 | const [shouldAsk, setShouldAsk] = React.useState(null); 54 | const [asked, setAsked] = React.useState(false); 55 | 56 | if (!asked) { 57 | setAsked(true); 58 | (async () => { 59 | for (const permission of wantedPermissions) { 60 | const { status } = await Permissions.getAsync(permission); 61 | logger.info(`Permissions status for ${permission}: ${status}`); 62 | if (status === Permissions.PermissionStatus.UNDETERMINED) { 63 | setShouldAsk(true); 64 | return; 65 | } else { 66 | dispatch(setPermission(permission, status === Permissions.PermissionStatus.GRANTED)); 67 | } 68 | } 69 | 70 | setShouldAsk(false); 71 | })(); 72 | } 73 | 74 | if (shouldAsk === null) { 75 | return (); 76 | } else { 77 | return null; 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /src/SettingsGate.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { useSelector } from "react-redux"; 6 | import NetInfo, { NetInfoState } from "@react-native-community/netinfo"; 7 | 8 | import moment from "moment"; 9 | import "moment/locale/en-gb"; 10 | 11 | import { StoreState, store } from "./store"; 12 | import { setConnectionInfo } from "./store/actions"; 13 | import { logger, setLogLevel } from "./logging"; 14 | 15 | function handleConnectivityChange(connectionInfo: NetInfoState) { 16 | logger.info(`ConnectionfInfo: ${connectionInfo.isConnected} ${connectionInfo.type}`); 17 | store.dispatch(setConnectionInfo({ type: connectionInfo.type, isConnected: connectionInfo.isConnected })); 18 | } 19 | 20 | export default React.memo(function SettingsGate(props: React.PropsWithChildren<{}>) { 21 | const settings = useSelector((state: StoreState) => state.settings); 22 | 23 | React.useEffect(() => { 24 | setLogLevel(settings.logLevel); 25 | }, [settings.logLevel]); 26 | 27 | React.useEffect(() => { 28 | moment.locale(settings.locale); 29 | }, [settings.locale]); 30 | 31 | // Not really settings but the app's general state. 32 | React.useEffect(() => { 33 | const unsubscribe = NetInfo.addEventListener(handleConnectivityChange); 34 | return unsubscribe; 35 | }, []); 36 | 37 | return ( 38 | <>{props.children} 39 | ); 40 | }); 41 | -------------------------------------------------------------------------------- /src/SyncGate.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { useSelector } from "react-redux"; 6 | 7 | import { useCredentials } from "./login"; 8 | import { useCredentials as useCredentialsEb } from "./credentials"; 9 | 10 | import LoadingIndicator from "./widgets/LoadingIndicator"; 11 | 12 | import { StoreState } from "./store"; 13 | 14 | import { syncInfoSelector } from "./SyncHandler"; 15 | 16 | export function useSyncGate() { 17 | const etesync = useCredentials(); 18 | const journals = useSelector((state: StoreState) => state.cache.journals); 19 | const entries = useSelector((state: StoreState) => state.cache.entries); 20 | const userInfo = useSelector((state: StoreState) => state.cache.userInfo); 21 | const syncCount = useSelector((state: StoreState) => state.syncCount); 22 | const syncStatus = useSelector((state: StoreState) => state.syncStatus); 23 | 24 | if ((syncCount > 0) || !etesync || !journals || !entries || !userInfo) { 25 | return (); 26 | } 27 | 28 | syncInfoSelector({ etesync, entries, journals, userInfo }); 29 | 30 | return null; 31 | } 32 | 33 | export function useSyncGateEb() { 34 | const etebase = useCredentialsEb(); 35 | const syncCount = useSelector((state: StoreState) => state.syncCount); 36 | const syncStatus = useSelector((state: StoreState) => state.syncStatus); 37 | 38 | if ((syncCount > 0) || !etebase) { 39 | return (); 40 | } 41 | 42 | return null; 43 | } 44 | -------------------------------------------------------------------------------- /src/SyncHandler.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { createSelector } from "reselect"; 5 | 6 | import * as EteSync from "etesync"; 7 | import { byte } from "etesync"; 8 | 9 | import { store, JournalsData, EntriesData, CredentialsData, UserInfoData, SyncInfoItem } from "./store"; 10 | import { setSyncInfoCollection, setSyncInfoItem, unsetSyncInfoCollection } from "./store/actions"; 11 | 12 | interface SyncInfoSelectorProps { 13 | etesync: CredentialsData; 14 | journals: JournalsData; 15 | entries: EntriesData; 16 | userInfo: UserInfoData; 17 | } 18 | 19 | export const syncInfoSelector = createSelector( 20 | (props: SyncInfoSelectorProps) => props.etesync, 21 | (props: SyncInfoSelectorProps) => props.journals, 22 | (props: SyncInfoSelectorProps) => props.entries, 23 | (props: SyncInfoSelectorProps) => props.userInfo, 24 | (etesync, journals, entries, userInfo) => { 25 | const syncInfoCollection = store.getState().cache.syncInfoCollection; 26 | const syncInfoItem = store.getState().cache.syncInfoItem; 27 | const derived = etesync.encryptionKey; 28 | const userInfoCryptoManager = userInfo.getCryptoManager(etesync.encryptionKey); 29 | let asymmetricCryptoManager: EteSync.AsymmetricCryptoManager; 30 | try { 31 | userInfo.verify(userInfoCryptoManager); 32 | } catch (error) { 33 | if (error instanceof EteSync.IntegrityError) { 34 | throw new EteSync.EncryptionPasswordError(error.message); 35 | } else { 36 | throw error; 37 | } 38 | } 39 | 40 | const handled = {}; 41 | journals.forEach((journal) => { 42 | const journalEntries = entries.get(journal.uid); 43 | let prevUid: string | null = null; 44 | 45 | if (!journalEntries) { 46 | return; 47 | } 48 | 49 | let cryptoManager: EteSync.CryptoManager; 50 | let derivedJournalKey: byte[] | undefined; 51 | if (journal.key) { 52 | if (!asymmetricCryptoManager) { 53 | const keyPair = userInfo.getKeyPair(userInfoCryptoManager); 54 | asymmetricCryptoManager = new EteSync.AsymmetricCryptoManager(keyPair); 55 | } 56 | derivedJournalKey = asymmetricCryptoManager.decryptBytes(journal.key); 57 | cryptoManager = EteSync.CryptoManager.fromDerivedKey(derivedJournalKey, journal.version); 58 | } else { 59 | cryptoManager = new EteSync.CryptoManager(derived, journal.uid, journal.version); 60 | } 61 | 62 | const collectionInfo = journal.getInfo(cryptoManager); 63 | store.dispatch(setSyncInfoCollection(etesync, collectionInfo)); 64 | 65 | journalEntries.forEach((entry: EteSync.Entry) => { 66 | const cacheEntry = syncInfoItem.getIn([journal.uid, entry.uid]); 67 | if (cacheEntry) { 68 | prevUid = entry.uid; 69 | return cacheEntry; 70 | } 71 | 72 | const syncEntry = entry.getSyncEntry(cryptoManager, prevUid); 73 | prevUid = entry.uid; 74 | 75 | store.dispatch(setSyncInfoItem(etesync, journal.uid, syncEntry as SyncInfoItem)); 76 | return syncEntry; 77 | }); 78 | 79 | handled[journal.uid] = true; 80 | }); 81 | 82 | for (const collection of syncInfoCollection.values()) { 83 | if (!handled[collection.uid]) { 84 | store.dispatch(unsetSyncInfoCollection(etesync, collection)); 85 | } 86 | } 87 | } 88 | ); 89 | -------------------------------------------------------------------------------- /src/components/EncryptionLoginForm.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { View } from "react-native"; 6 | import { Text, HelperText, Button } from "react-native-paper"; 7 | import PasswordInput from "../widgets/PasswordInput"; 8 | 9 | 10 | interface FormErrors { 11 | encryptionPassword?: string; 12 | } 13 | 14 | interface PropsType { 15 | onSubmit: (encryptionPassword: string) => void; 16 | } 17 | 18 | export default function _EncryptionLognForm(props: PropsType) { 19 | const [errors, setErrors] = React.useState({}); 20 | const [encryptionPassword, setEncryptionPassword] = React.useState(); 21 | 22 | function onSave() { 23 | const saveErrors: FormErrors = {}; 24 | const fieldRequired = "This field is required!"; 25 | 26 | if (!encryptionPassword) { 27 | saveErrors.encryptionPassword = fieldRequired; 28 | } 29 | 30 | if (Object.keys(saveErrors).length > 0) { 31 | setErrors(saveErrors); 32 | return; 33 | } 34 | 35 | props.onSubmit(encryptionPassword!); 36 | } 37 | 38 | return ( 39 | 40 | 48 | 52 | {errors.encryptionPassword} 53 | 54 | 55 | 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/components/JournalListScreenEb.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { useSelector } from "react-redux"; 6 | import { View } from "react-native"; 7 | import { Avatar, IconButton, Card, Menu, List, Colors, Text } from "react-native-paper"; 8 | import { useNavigation } from "@react-navigation/native"; 9 | 10 | import moment from "moment"; 11 | 12 | import { defaultColor } from "../helpers"; 13 | 14 | import ScrollView from "../widgets/ScrollView"; 15 | import ColorBox from "../widgets/ColorBox"; 16 | import { useSyncGateEb } from "../SyncGate"; 17 | 18 | import { StoreState } from "../store"; 19 | 20 | const backgroundPrimary = Colors.amber700; 21 | 22 | const JournalsMoreMenu = React.memo(function _JournalsMoreMenu(props: { colType: string }) { 23 | const [showMenu, setShowMenu] = React.useState(false); 24 | const navigation = useNavigation(); 25 | 26 | return ( 27 | setShowMenu(false)} 30 | anchor={( 31 | setShowMenu(true)} /> 32 | )} 33 | > 34 | { 36 | setShowMenu(false); 37 | navigation.navigate("CollectionNew", { colType: props.colType }); 38 | }} 39 | title="Create new" 40 | /> 41 | 42 | ); 43 | }); 44 | 45 | 46 | export default function JournalListScreen() { 47 | const navigation = useNavigation(); 48 | const syncGate = useSyncGateEb(); 49 | const decryptedCollections = useSelector((state: StoreState) => state.cache2.decryptedCollections); 50 | const lastSync = useSelector((state: StoreState) => state.sync.lastSync); 51 | 52 | if (syncGate) { 53 | return syncGate; 54 | } 55 | 56 | const collectionsMap = { 57 | "etebase.vevent": [] as React.ReactNode[], 58 | "etebase.vcard": [] as React.ReactNode[], 59 | "etebase.vtodo": [] as React.ReactNode[], 60 | }; 61 | 62 | for (const [uid, { meta, collectionType }] of decryptedCollections.entries()) { 63 | if (!collectionsMap[collectionType]) { 64 | continue; 65 | } 66 | const readOnly = false; // FIXME-eb 67 | const shared = false; // FIXME-eb 68 | 69 | let colorBox: any; 70 | switch (collectionType) { 71 | case "etebase.vevent": 72 | case "etebase.vtodo": 73 | colorBox = ( 74 | 75 | ); 76 | break; 77 | } 78 | 79 | const rightIcon = (props: any) => ( 80 | 81 | {shared && 82 | 83 | } 84 | {readOnly && 85 | 86 | } 87 | {colorBox} 88 | 89 | ); 90 | 91 | collectionsMap[collectionType].push( 92 | navigation.navigate("Collection", { colUid: uid })} 95 | title={meta.name} 96 | right={rightIcon} 97 | /> 98 | ); 99 | } 100 | 101 | const cards = [ 102 | { 103 | title: "Address Books", 104 | lookup: "etebase.vcard", 105 | icon: "contacts", 106 | }, 107 | { 108 | title: "Calendars", 109 | lookup: "etebase.vevent", 110 | icon: "calendar", 111 | }, 112 | { 113 | title: "Tasks", 114 | lookup: "etebase.vtodo", 115 | icon: "format-list-checkbox", 116 | }, 117 | ]; 118 | 119 | const shadowStyle = { 120 | shadowColor: "#000", 121 | shadowOffset: { 122 | width: 0, 123 | height: 1, 124 | }, 125 | shadowOpacity: 0.20, 126 | shadowRadius: 1.41, 127 | 128 | elevation: 2, 129 | }; 130 | 131 | return ( 132 | 133 | Last sync: {lastSync ? moment(lastSync).format("lll") : "never"} 134 | {cards.map((card) => ( 135 | 136 | ( 141 | 142 | 143 | 144 | )} 145 | right={() => ( 146 | 147 | )} 148 | /> 149 | {collectionsMap[card.lookup]} 150 | 151 | ))} 152 | 153 | ); 154 | } 155 | -------------------------------------------------------------------------------- /src/components/WebviewKeygen.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { WebView } from "react-native-webview"; 6 | 7 | interface Keys { 8 | privateKey: string; 9 | publicKey: string; 10 | error?: string; 11 | } 12 | 13 | interface PropsType { 14 | onFinish: (keys: Keys) => void; 15 | } 16 | 17 | export default React.memo(function WebviewKeygen(props: PropsType) { 18 | return ( 19 | 24 | 25 | 26 | 59 | 60 | 61 | ` }} 62 | onMessage={({ nativeEvent: state }) => { 63 | const keys = JSON.parse(state.data); 64 | props.onFinish(keys); 65 | }} 66 | /> 67 | ); 68 | }); 69 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export const appName = "EteSync"; 5 | export const homePage = "https://www.etesync.com/"; 6 | export const faq = homePage + "faq/"; 7 | export const dashboard = homePage + "dashboard/"; 8 | export const sourceCode = "https://github.com/etesync/ios"; 9 | export const reportIssue = sourceCode + "/issues"; 10 | export const contactEmail = "contact-ios@etesync.com"; 11 | export const reportsEmail = "reports-ios@etesync.com"; 12 | 13 | export const forgotPassword = "https://www.etesync.com/accounts/password/reset/"; 14 | 15 | export const serviceApiBase = "https://api.etesync.com/"; 16 | export const serviceApiBaseEb = "https://api.etebase.com/partner/etesync/"; 17 | 18 | // In generic mode we don't have anything etesync.com specific 19 | export const genericMode = true; 20 | // Sync app mode is an experimental mode for controlling sync settings 21 | export const syncAppMode = true; 22 | -------------------------------------------------------------------------------- /src/credentials.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2017 Etebase Authors 2 | // SPDX-License-Identifier: AGPL-3.0-only 3 | 4 | import { useSelector } from "react-redux"; 5 | import { createSelector } from "reselect"; 6 | 7 | import * as Etebase from "etebase"; 8 | 9 | import * as store from "./store"; 10 | import { usePromiseMemo } from "./helpers"; 11 | 12 | export const credentialsSelector = createSelector( 13 | (state: store.StoreState) => state.credentials2.storedSession, 14 | (storedSession) => { 15 | if (storedSession) { 16 | return Etebase.Account.restore(storedSession); 17 | } else { 18 | return Promise.resolve(null); 19 | } 20 | } 21 | ); 22 | 23 | export function useCredentials() { 24 | const credentialsPromise = useSelector(credentialsSelector); 25 | return usePromiseMemo(credentialsPromise, [credentialsPromise]); 26 | } 27 | -------------------------------------------------------------------------------- /src/etesync-helpers.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as EteSync from "etesync"; 5 | 6 | import { CredentialsData, UserInfoData } from "./store"; 7 | import { addEntries } from "./store/actions"; 8 | 9 | export function createJournalEntry( 10 | etesync: CredentialsData, 11 | userInfo: UserInfoData, 12 | journal: EteSync.Journal, 13 | prevUid: string | null, 14 | action: EteSync.SyncEntryAction, 15 | content: string) { 16 | 17 | const syncEntry = new EteSync.SyncEntry(); 18 | syncEntry.action = action; 19 | 20 | syncEntry.content = content; 21 | return createJournalEntryFromSyncEntry(etesync, userInfo, journal, prevUid, syncEntry); 22 | } 23 | 24 | export function createJournalEntryFromSyncEntry( 25 | etesync: CredentialsData, 26 | userInfo: UserInfoData, 27 | journal: EteSync.Journal, 28 | prevUid: string | null, 29 | syncEntry: EteSync.SyncEntry) { 30 | 31 | const derived = etesync.encryptionKey; 32 | 33 | const keyPair = userInfo.getKeyPair(userInfo.getCryptoManager(derived)); 34 | const cryptoManager = journal.getCryptoManager(derived, keyPair); 35 | const entry = new EteSync.Entry(); 36 | entry.setSyncEntry(cryptoManager, syncEntry, prevUid); 37 | 38 | return entry; 39 | } 40 | 41 | export function addJournalEntry( 42 | etesync: CredentialsData, 43 | userInfo: UserInfoData, 44 | journal: EteSync.Journal, 45 | prevUid: string | null, 46 | action: EteSync.SyncEntryAction, 47 | content: string) { 48 | 49 | const entry = createJournalEntry(etesync, userInfo, journal, prevUid, action, content); 50 | return addEntries(etesync, journal.uid, [entry], prevUid); 51 | } 52 | -------------------------------------------------------------------------------- /src/helpers.test.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { colorHtmlToInt, colorIntToHtml } from "./helpers"; 5 | 6 | it("Color conversion", () => { 7 | const testColors = [ 8 | "#aaaaaaaa", 9 | "#00aaaaaa", 10 | "#0000aaaa", 11 | "#000000aa", 12 | "#00000000", 13 | "#bb00bbbb", 14 | "#bb0000bb", 15 | "#bb000000", 16 | "#11110011", 17 | "#11110000", 18 | "#11111100", 19 | ]; 20 | 21 | for (const color of testColors) { 22 | expect(color).toEqual(colorIntToHtml(colorHtmlToInt(color))); 23 | } 24 | }); 25 | 26 | -------------------------------------------------------------------------------- /src/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/966c112bd6a92daaee2ab128cea29118a13188d8/src/images/icon.png -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { Provider } from "react-redux"; 6 | import { PersistGate } from "redux-persist/es/integration/react"; 7 | import App from "./App"; 8 | 9 | import "react-native-etebase"; 10 | import * as Etebase from "etebase"; 11 | import { store, persistor } from "./store"; 12 | 13 | function MyPersistGate(props: React.PropsWithChildren<{}>) { 14 | const [loading, setLoading] = React.useState(true); 15 | 16 | React.useEffect(() => { 17 | Etebase.ready.then(() => { 18 | setLoading(false); 19 | persistor.persist(); 20 | }); 21 | }, []); 22 | 23 | if (loading) { 24 | return (); 25 | } 26 | 27 | return ( 28 | 29 | {props.children} 30 | 31 | ); 32 | } 33 | 34 | class Index extends React.Component { 35 | public render() { 36 | return ( 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | } 45 | 46 | export default Index; 47 | -------------------------------------------------------------------------------- /src/logging.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { AsyncStorage } from "react-native"; 5 | 6 | export enum LogLevel { 7 | Off = 0, 8 | Critical, 9 | Warning, 10 | Info, 11 | Debug, 12 | } 13 | 14 | let logLevel = (__DEV__) ? LogLevel.Debug : LogLevel.Off; 15 | 16 | export function setLogLevel(level: LogLevel) { 17 | if (!__DEV__) { 18 | logLevel = level; 19 | } 20 | } 21 | 22 | function shouldLog(messageLevel: LogLevel) { 23 | return messageLevel <= logLevel; 24 | } 25 | 26 | function logPrint(messageLevel: LogLevel, message: any) { 27 | if (!shouldLog(messageLevel)) { 28 | return; 29 | } 30 | 31 | switch (messageLevel) { 32 | case LogLevel.Critical: 33 | case LogLevel.Warning: 34 | console.warn(message); 35 | break; 36 | default: 37 | console.log(message); 38 | } 39 | } 40 | 41 | const logPrefix = "__logging_"; 42 | 43 | function logToBuffer(messageLevel: LogLevel, message: any) { 44 | if (!shouldLog(messageLevel)) { 45 | return; 46 | } 47 | 48 | AsyncStorage.setItem(`${logPrefix}${new Date().toISOString()}`, `[${LogLevel[messageLevel].substr(0, 1)}] ${message}`); 49 | } 50 | 51 | async function getLogKeys() { 52 | const keys = await AsyncStorage.getAllKeys(); 53 | return keys.filter((key) => key.startsWith(logPrefix)); 54 | } 55 | 56 | export async function getLogs() { 57 | const wantedKeys = await getLogKeys(); 58 | if (wantedKeys.length === 0) { 59 | return []; 60 | } 61 | 62 | const wantedItems = await AsyncStorage.multiGet(wantedKeys); 63 | return wantedItems.sort(([a], [b]) => { 64 | return a.localeCompare(b); 65 | }).map(([_key, value]) => value); 66 | } 67 | 68 | export async function clearLogs() { 69 | const wantedKeys = await getLogKeys(); 70 | if (wantedKeys.length === 0) { 71 | return; 72 | } 73 | await AsyncStorage.multiRemove(wantedKeys); 74 | } 75 | 76 | const logHandler = (__DEV__) ? logPrint : logToBuffer; 77 | 78 | class Logger { 79 | public debug(message: string) { 80 | logHandler(LogLevel.Debug, message); 81 | } 82 | 83 | public info(message: string) { 84 | logHandler(LogLevel.Info, message); 85 | } 86 | 87 | public warn(message: string) { 88 | logHandler(LogLevel.Warning, message); 89 | } 90 | 91 | public critical(message: string) { 92 | logHandler(LogLevel.Critical, message); 93 | } 94 | } 95 | 96 | export const logger = new Logger(); 97 | -------------------------------------------------------------------------------- /src/login/index.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { shallowEqual, useSelector } from "react-redux"; 5 | import { createSelector } from "reselect"; 6 | 7 | import * as store from "../store"; 8 | 9 | export const remoteCredentialsSelector = createSelector( 10 | (state: store.StoreState) => state.credentials.credentials ?? state.legacyCredentials.credentials, 11 | (state: store.StoreState) => state.credentials.serviceApiUrl ?? state.legacyCredentials.serviceApiUrl, 12 | (credentials, serviceApiUrl) => { 13 | if (!credentials) { 14 | return null; 15 | } 16 | 17 | const ret: store.CredentialsDataRemote = { 18 | credentials, 19 | serviceApiUrl, 20 | }; 21 | return ret; 22 | } 23 | ); 24 | 25 | export function useRemoteCredentials() { 26 | return useSelector(remoteCredentialsSelector, shallowEqual); 27 | } 28 | 29 | export const credentialsSelector = createSelector( 30 | (state: store.StoreState) => remoteCredentialsSelector(state), 31 | (state: store.StoreState) => state.encryptionKey.encryptionKey ?? state.legacyEncryptionKey.key, 32 | (remoteCredentials, encryptionKey) => { 33 | if (!remoteCredentials || !encryptionKey) { 34 | return null; 35 | } 36 | 37 | const ret: store.CredentialsData = { 38 | ...remoteCredentials, 39 | encryptionKey, 40 | }; 41 | return ret; 42 | } 43 | ); 44 | 45 | export function useCredentials() { 46 | return useSelector(credentialsSelector, shallowEqual); 47 | } 48 | -------------------------------------------------------------------------------- /src/store/index.test.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { addEntries, fetchEntries } from "./actions"; 5 | import { entries, EntriesData } from "./reducers"; 6 | 7 | import { Map } from "immutable"; 8 | 9 | import * as EteSync from "etesync"; 10 | 11 | it("Entries reducer", () => { 12 | const jId = "24324324324"; 13 | let state = Map({}) as EntriesData; 14 | 15 | const entry = new EteSync.Entry(); 16 | entry.deserialize({ 17 | content: "someContent", 18 | uid: "6355209e2a2c26a6c1e6e967c2032737d538f602cf912474da83a2902f8a0a83", 19 | }); 20 | 21 | const action = { 22 | type: fetchEntries.toString(), 23 | meta: { journal: jId, prevUid: null as string | null }, 24 | payload: [entry], 25 | }; 26 | 27 | let journal; 28 | let entry2; 29 | 30 | state = entries(state, action as any); 31 | journal = state.get(jId)!; 32 | entry2 = journal.get(0)!; 33 | expect(entry2.serialize()).toEqual(entry.serialize()); 34 | 35 | // We replace if there's no prevUid 36 | state = entries(state, action as any); 37 | journal = state.get(jId)!; 38 | entry2 = journal.get(0)!; 39 | expect(entry2.serialize()).toEqual(entry.serialize()); 40 | expect(journal.size).toBe(1); 41 | 42 | // We extend if prevUid is set 43 | action.meta.prevUid = entry.uid; 44 | state = entries(state, action as any); 45 | journal = state.get(jId)!; 46 | expect(journal.size).toBe(2); 47 | 48 | // Creating entries should also work the same 49 | action.type = addEntries.toString(); 50 | state = entries(state, action as any); 51 | journal = state.get(jId)!; 52 | expect(journal.size).toBe(3); 53 | }); 54 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { createStore, applyMiddleware } from "redux"; 5 | import { persistStore } from "redux-persist"; 6 | import thunkMiddleware from "redux-thunk"; 7 | import { createLogger } from "redux-logger"; 8 | import { useDispatch } from "react-redux"; 9 | import { ActionMeta } from "redux-actions"; 10 | 11 | import promiseMiddleware from "./promise-middleware"; 12 | 13 | import reducers from "./construct"; 14 | import * as actions from "./actions"; 15 | 16 | // Workaround babel limitation 17 | export * from "./reducers"; 18 | export * from "./construct"; 19 | 20 | const middleware = [ 21 | thunkMiddleware, 22 | promiseMiddleware, 23 | ]; 24 | 25 | if (__DEV__) { 26 | const ignoreActions = [ 27 | "persist/PERSIST", 28 | "persist/REHYDRATE", 29 | actions.setSyncStateJournal.toString(), 30 | actions.unsetSyncStateJournal.toString(), 31 | actions.setSyncStateEntry.toString(), 32 | actions.unsetSyncStateEntry.toString(), 33 | actions.setSyncInfoCollection.toString(), 34 | actions.unsetSyncInfoCollection.toString(), 35 | actions.setSyncInfoItem.toString(), 36 | actions.unsetSyncInfoItem.toString(), 37 | ]; 38 | 39 | const predicate = (_: any, action: { type: string }) => { 40 | return !ignoreActions.includes(action.type); 41 | }; 42 | 43 | const logger = { 44 | log: (msg: string) => { 45 | if (msg[0] === "#") { 46 | console.log(msg); 47 | } 48 | }, 49 | }; 50 | 51 | middleware.push(createLogger({ 52 | predicate, 53 | logger, 54 | stateTransformer: () => "state", 55 | actionTransformer: ({ type, error, payload }) => ({ type, error, payload: (payload !== undefined) }), 56 | titleFormatter: (action: { type: string, error: any, payload: boolean }, time: string, took: number) => { 57 | let prefix = "->"; 58 | if (action.error) { 59 | prefix = "xx"; 60 | } else if (action.payload) { 61 | prefix = "=="; 62 | } 63 | return `# ${prefix} ${action.type} @ ${time} (in ${took.toFixed(2)} ms)`; 64 | }, 65 | colors: { 66 | title: false, 67 | prevState: false, 68 | action: false, 69 | nextState: false, 70 | error: false, 71 | }, 72 | })); 73 | } 74 | 75 | // FIXME: Hack, we don't actually return a promise when one is not passed. 76 | export function asyncDispatch(action: ActionMeta | T, V>): Promise> { 77 | return store.dispatch(action) as any; 78 | } 79 | 80 | export function useAsyncDispatch() { 81 | const dispatch = useDispatch(); 82 | return function (action: any): any { 83 | return dispatch(action) as any; 84 | } as typeof asyncDispatch; 85 | } 86 | 87 | export const store = createStore( 88 | reducers, 89 | applyMiddleware(...middleware) 90 | ); 91 | 92 | export const persistor = persistStore(store, { manualPersist: true } as any); 93 | -------------------------------------------------------------------------------- /src/store/promise-middleware.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | // Based on: https://github.com/acdlite/redux-promise/blob/master/src/index.js 5 | 6 | function isPromise(val: any): val is Promise { 7 | return val && typeof val.then === "function"; 8 | } 9 | 10 | export default function promiseMiddleware({ dispatch }: any) { 11 | return (next: any) => (action: any) => { 12 | if (isPromise(action.payload)) { 13 | dispatch({ ...action, payload: undefined }); 14 | 15 | return action.payload 16 | .then((result: any) => dispatch({ ...action, payload: result })) 17 | .catch((error: Error) => { 18 | dispatch({ ...action, payload: error, error: true }); 19 | return Promise.reject(error); 20 | }); 21 | } else { 22 | return next(action); 23 | } 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/sync/SyncManagerTaskList.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as Calendar from "expo-calendar"; 5 | 6 | import { calculateHashesForReminders, BatchAction, HashDictionary, processRemindersChanges } from "../EteSyncNative"; 7 | 8 | import { logger } from "../logging"; 9 | 10 | import { store } from "../store"; 11 | 12 | import { NativeTask, taskVobjectToNative, taskNativeToVobject } from "./helpers"; 13 | import { TaskType } from "../pim-types"; 14 | 15 | import { SyncManagerCalendarBase } from "./SyncManagerCalendar"; 16 | import { PushEntry } from "./SyncManagerBase"; 17 | 18 | export class SyncManagerTaskList extends SyncManagerCalendarBase { 19 | protected permissionsType = "TASKS"; 20 | protected collectionType = "etebase.vtodo"; 21 | protected collectionTypeDisplay = "Tasks"; 22 | protected entityType = Calendar.EntityTypes.REMINDER; 23 | 24 | protected async syncPush() { 25 | const storeState = store.getState(); 26 | const decryptedCollections = storeState.cache2.decryptedCollections; 27 | const syncStateJournals = storeState.sync.stateJournals; 28 | const syncStateEntries = storeState.sync.stateEntries; 29 | 30 | for (const [uid, { collectionType }] of decryptedCollections.entries()) { 31 | if (collectionType !== this.collectionType) { 32 | continue; 33 | } 34 | 35 | logger.info(`Pushing ${uid}`); 36 | 37 | const syncStateEntriesReverse = syncStateEntries.get(uid)!.mapEntries((_entry) => { 38 | const entry = _entry[1]; 39 | return [entry.localId, entry]; 40 | }).asMutable(); 41 | 42 | const syncEntries: PushEntry[] = []; 43 | 44 | const syncStateJournal = syncStateJournals.get(uid)!; 45 | const localId = syncStateJournal.localId; 46 | const existingReminders = await calculateHashesForReminders(localId); 47 | for (const [reminderId, reminderHash] of existingReminders) { 48 | const syncStateEntry = syncStateEntriesReverse.get(reminderId!); 49 | 50 | if (syncStateEntry?.lastHash !== reminderHash) { 51 | const _reminder = await Calendar.getReminderAsync(reminderId); 52 | const reminder = { ..._reminder, uid: (syncStateEntry) ? syncStateEntry.uid : _reminder.id! }; 53 | const syncEntry = await this.syncPushHandleAddChange(syncStateJournal, syncStateEntry, reminder, reminderHash); 54 | if (syncEntry) { 55 | syncEntries.push(syncEntry); 56 | } 57 | } 58 | 59 | if (syncStateEntry) { 60 | syncStateEntriesReverse.delete(syncStateEntry.uid); 61 | } 62 | } 63 | 64 | for (const syncStateEntry of syncStateEntriesReverse.values()) { 65 | // Deleted 66 | let existingReminder: Calendar.Reminder | undefined; 67 | try { 68 | existingReminder = await Calendar.getReminderAsync(syncStateEntry.localId); 69 | } catch (e) { 70 | // Skip 71 | } 72 | 73 | let shouldDelete = !existingReminder; 74 | if (existingReminder) { 75 | // FIXME: handle the case of the event still existing and on the same calendar. Probably means we are just not in the range. 76 | if (existingReminder.calendarId !== localId) { 77 | shouldDelete = true; 78 | } 79 | } 80 | 81 | if (shouldDelete) { 82 | // If the reminder still exists it means it's not deleted. 83 | const syncEntry = await this.syncPushHandleDeleted(syncStateJournal, syncStateEntry); 84 | if (syncEntry) { 85 | syncEntries.push(syncEntry); 86 | } 87 | } 88 | } 89 | 90 | await this.pushJournalEntries(syncStateJournal, syncEntries); 91 | } 92 | } 93 | 94 | protected contentToVobject(content: string) { 95 | return TaskType.parse(content); 96 | } 97 | 98 | protected vobjectToNative(vobject: TaskType) { 99 | return taskVobjectToNative(vobject); 100 | } 101 | 102 | protected nativeToVobject(nativeItem: NativeTask) { 103 | return taskNativeToVobject(nativeItem); 104 | } 105 | 106 | protected processSyncEntries(containerLocalId: string, batch: [BatchAction, NativeTask][]): Promise { 107 | return processRemindersChanges(containerLocalId, batch); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/sync/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export { SyncManagerAddressBook } from "./SyncManagerAddressBook"; 5 | export { SyncManagerCalendar } from "./SyncManagerCalendar"; 6 | export { SyncManager } from "./SyncManager"; 7 | -------------------------------------------------------------------------------- /src/sync/legacy/SyncManagerTaskList.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as EteSync from "etesync"; 5 | import * as Calendar from "expo-calendar"; 6 | 7 | import { calculateHashesForReminders, BatchAction, HashDictionary, processRemindersChanges } from "../../EteSyncNative"; 8 | 9 | import { logger } from "../../logging"; 10 | 11 | import { store } from "../../store"; 12 | 13 | import { NativeTask, taskVobjectToNative, taskNativeToVobject } from "../helpers"; 14 | import { TaskType } from "../../pim-types"; 15 | 16 | import { SyncManagerCalendarBase } from "./SyncManagerCalendar"; 17 | import { PushEntry } from "./SyncManagerBase"; 18 | 19 | export class SyncManagerTaskList extends SyncManagerCalendarBase { 20 | protected collectionType = "TASKS"; 21 | protected entityType = Calendar.EntityTypes.REMINDER; 22 | 23 | protected async syncPush() { 24 | const storeState = store.getState(); 25 | const syncInfoCollections = storeState.cache.syncInfoCollection; 26 | const syncStateJournals = storeState.sync.stateJournals; 27 | const syncStateEntries = storeState.sync.stateEntries; 28 | 29 | for (const collection of syncInfoCollections.values()) { 30 | const uid = collection.uid; 31 | 32 | if (collection.type !== this.collectionType) { 33 | continue; 34 | } 35 | 36 | logger.info(`Pushing ${uid}`); 37 | 38 | const syncStateEntriesReverse = syncStateEntries.get(uid)!.mapEntries((_entry) => { 39 | const entry = _entry[1]; 40 | return [entry.localId, entry]; 41 | }).asMutable(); 42 | 43 | const syncEntries: PushEntry[] = []; 44 | 45 | const syncStateJournal = syncStateJournals.get(uid)!; 46 | const localId = syncStateJournal.localId; 47 | const existingReminders = await calculateHashesForReminders(localId); 48 | for (const [reminderId, reminderHash] of existingReminders) { 49 | const syncStateEntry = syncStateEntriesReverse.get(reminderId!); 50 | 51 | if (syncStateEntry?.lastHash !== reminderHash) { 52 | const _reminder = await Calendar.getReminderAsync(reminderId); 53 | const reminder = { ..._reminder, uid: (syncStateEntry) ? syncStateEntry.uid : _reminder.id! }; 54 | const syncEntry = this.syncPushHandleAddChange(syncStateJournal, syncStateEntry, reminder, reminderHash); 55 | if (syncEntry) { 56 | syncEntries.push(syncEntry); 57 | } 58 | } 59 | 60 | if (syncStateEntry) { 61 | syncStateEntriesReverse.delete(syncStateEntry.uid); 62 | } 63 | } 64 | 65 | for (const syncStateEntry of syncStateEntriesReverse.values()) { 66 | // Deleted 67 | let existingReminder: Calendar.Reminder | undefined; 68 | try { 69 | existingReminder = await Calendar.getReminderAsync(syncStateEntry.localId); 70 | } catch (e) { 71 | // Skip 72 | } 73 | 74 | let shouldDelete = !existingReminder; 75 | if (existingReminder) { 76 | // FIXME: handle the case of the event still existing and on the same calendar. Probably means we are just not in the range. 77 | if (existingReminder.calendarId !== localId) { 78 | shouldDelete = true; 79 | } 80 | } 81 | 82 | if (shouldDelete) { 83 | // If the reminder still exists it means it's not deleted. 84 | const syncEntry = this.syncPushHandleDeleted(syncStateJournal, syncStateEntry); 85 | if (syncEntry) { 86 | syncEntries.push(syncEntry); 87 | } 88 | } 89 | } 90 | 91 | await this.pushJournalEntries(syncStateJournal, syncEntries); 92 | } 93 | } 94 | 95 | protected syncEntryToVobject(syncEntry: EteSync.SyncEntry) { 96 | return TaskType.parse(syncEntry.content); 97 | } 98 | 99 | protected vobjectToNative(vobject: TaskType) { 100 | return taskVobjectToNative(vobject); 101 | } 102 | 103 | protected nativeToVobject(nativeItem: NativeTask) { 104 | return taskNativeToVobject(nativeItem); 105 | } 106 | 107 | protected processSyncEntries(containerLocalId: string, batch: [BatchAction, NativeTask][]): Promise { 108 | return processRemindersChanges(containerLocalId, batch); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/types/redux-persist.d.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | declare module "redux-persist"; 5 | declare module "redux-persist/lib/storage/session"; 6 | declare module "redux-persist/es/integration/react"; 7 | -------------------------------------------------------------------------------- /src/widgets/Alert.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { View, ViewProps, StyleSheet } from "react-native"; 6 | import { Text, useTheme } from "react-native-paper"; 7 | import Icon from "react-native-vector-icons/MaterialCommunityIcons"; 8 | 9 | type PropsType = ViewProps & { 10 | severity: "error" | "warning" | "info" | "success"; 11 | }; 12 | 13 | export default function Alert(props_: React.PropsWithChildren) { 14 | const theme = useTheme(); 15 | const { children, style, severity, ...props } = props_; 16 | const icons = { 17 | error: "information-outline", 18 | warning: "alert-outline", 19 | info: "information-outline", 20 | success: "checkbox-marked-circle-outline", 21 | }; 22 | const stylesColor = theme.dark ? stylesDark : stylesLight; 23 | 24 | return ( 25 | 26 | 27 | 31 | {children} 32 | 33 | 34 | ); 35 | } 36 | 37 | const styles = StyleSheet.create({ 38 | root: { 39 | flexDirection: "row", 40 | alignItems: "center", 41 | paddingHorizontal: 16, 42 | paddingVertical: 6, 43 | borderRadius: 4, 44 | }, 45 | icon: { 46 | marginRight: 12, 47 | }, 48 | text: { 49 | flex: 1, 50 | }, 51 | }); 52 | 53 | const stylesIcon = StyleSheet.create({ 54 | error: { 55 | color: "#f44336", 56 | }, 57 | warning: { 58 | color: "#ff9800", 59 | }, 60 | info: { 61 | color: "#2196f3", 62 | }, 63 | success: { 64 | color: "#4caf50", 65 | }, 66 | }); 67 | 68 | const stylesLight = StyleSheet.create({ 69 | error: { 70 | color: "rgb(97, 26, 21)", 71 | backgroundColor: "rgb(253, 236, 234)", 72 | }, 73 | warning: { 74 | color: "rgb(102, 60, 0)", 75 | backgroundColor: "rgb(255, 244, 229)", 76 | }, 77 | info: { 78 | color: "rgb(13, 60, 97)", 79 | backgroundColor: "rgb(232, 244, 253)", 80 | }, 81 | success: { 82 | color: "rgb(30, 70, 32)", 83 | backgroundColor: "rgb(237, 247, 237)", 84 | }, 85 | }); 86 | 87 | const stylesDark = StyleSheet.create({ 88 | error: { 89 | color: "rgb(250, 179, 174)", 90 | backgroundColor: "rgb(24, 6, 5)", 91 | }, 92 | warning: { 93 | color: "rgb(255, 213, 153)", 94 | backgroundColor: "rgb(25, 15, 0)", 95 | }, 96 | info: { 97 | color: "rgb(166, 213, 250)", 98 | backgroundColor: "rgb(3, 14, 24)", 99 | }, 100 | success: { 101 | color: "rgb(183, 223, 185)", 102 | backgroundColor: "rgb(7, 17, 7)", 103 | }, 104 | }); 105 | -------------------------------------------------------------------------------- /src/widgets/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { Checkbox as PaperCheckbox, Paragraph, TouchableRipple } from "react-native-paper"; 6 | import { View, StyleProp, ViewStyle } from "react-native"; 7 | 8 | interface PropsType { 9 | style?: StyleProp; 10 | title: string; 11 | status: boolean; 12 | onPress: () => void; 13 | } 14 | 15 | export default function Checkbox(props: PropsType) { 16 | return ( 17 | 21 | 22 | {props.title} 23 | 24 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/widgets/ColorBox.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { View, StyleProp, ViewStyle } from "react-native"; 6 | 7 | interface PropsType { 8 | color: string; 9 | size?: number; 10 | style?: StyleProp; 11 | } 12 | 13 | export default function ColorBox(props: PropsType) { 14 | const size = (props.size) ? props.size : 64; 15 | const style = { ...(props.style as any), backgroundColor: props.color, width: size, height: size }; 16 | 17 | return ( 18 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/widgets/ColorPicker.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { View } from "react-native"; 6 | import { TouchableRipple, HelperText } from "react-native-paper"; 7 | 8 | import TextInput from "./TextInput"; 9 | import ColorBox from "./ColorBox"; 10 | import { colorHtmlToInt } from "../helpers"; 11 | 12 | interface PropsType { 13 | color: string; 14 | defaultColor: string; 15 | label?: string; 16 | placeholder?: string; 17 | error?: string; 18 | onChange: (color: string) => void; 19 | } 20 | 21 | export default function ColorPicker(props: PropsType) { 22 | const colors = [ 23 | [ 24 | "#F44336", 25 | "#E91E63", 26 | "#673AB7", 27 | "#3F51B5", 28 | "#2196F3", 29 | ], 30 | [ 31 | "#03A9F4", 32 | "#4CAF50", 33 | "#8BC34A", 34 | "#FFEB3B", 35 | "#FF9800", 36 | ], 37 | ]; 38 | const color = props.color; 39 | 40 | return ( 41 | 42 | {colors.map((colorGroup, idx) => ( 43 | 44 | {colorGroup.map((colorOption) => ( 45 | props.onChange(colorOption)} 49 | > 50 | 51 | 52 | ))} 53 | 54 | ))} 55 | 56 | 57 | 65 | 69 | {props.error} 70 | 71 | 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/widgets/ConfirmationDialog.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { Keyboard } from "react-native"; 6 | import { Card, Portal, Modal, Button, ProgressBar, Paragraph, useTheme } from "react-native-paper"; 7 | 8 | import { isPromise, useIsMounted } from "../helpers"; 9 | 10 | interface PropsType { 11 | title: string; 12 | children: React.ReactNode | React.ReactNode[]; 13 | visible: boolean; 14 | dismissable?: boolean; 15 | onCancel?: () => void; 16 | onOk?: () => void | Promise; 17 | labelCancel?: string; 18 | labelOk?: string; 19 | loading?: boolean; 20 | loadingText?: string; 21 | } 22 | 23 | export default React.memo(function ConfirmationDialog(props: PropsType) { 24 | const isMounted = useIsMounted(); 25 | const [loading, setLoading] = React.useState(props.loading ?? false); 26 | const [error, setError] = React.useState(undefined); 27 | const theme = useTheme(); 28 | const labelCancel = props.labelCancel ?? "Cancel"; 29 | const labelOk = props.labelOk ?? "OK"; 30 | const loadingText = props.loadingText ?? "Loading..."; 31 | const buttonThemeOverride = { colors: { primary: theme.colors.accent } }; 32 | 33 | React.useEffect(() => { 34 | Keyboard.dismiss(); 35 | }, [props.visible]); 36 | 37 | function onOk() { 38 | const ret = props.onOk?.(); 39 | if (isPromise(ret)) { 40 | // If it's a promise, we update the loading state based on it. 41 | setLoading(true); 42 | ret.catch((e) => { 43 | if (isMounted.current) { 44 | setError(e.toString()); 45 | } 46 | }).finally(() => { 47 | if (isMounted.current) { 48 | setLoading(false); 49 | } 50 | }); 51 | } 52 | } 53 | 54 | let content: React.ReactNode | React.ReactNode[]; 55 | if (error !== undefined) { 56 | content = ( 57 | Error: {error.toString()} 58 | ); 59 | } else if (loading) { 60 | content = ( 61 | <> 62 | {loadingText} 63 | 64 | 65 | ); 66 | } else { 67 | content = props.children; 68 | } 69 | 70 | return ( 71 | 72 | 77 | 78 | 79 | 80 | {content} 81 | 82 | 83 | {props.onCancel && 84 | 85 | } 86 | {!error && props.onOk && 87 | 88 | } 89 | 90 | 91 | 92 | 93 | ); 94 | }); 95 | -------------------------------------------------------------------------------- /src/widgets/Container.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { ViewProps, View } from "react-native"; 6 | import { useTheme } from "react-native-paper"; 7 | 8 | export default function Container(inProps: React.PropsWithChildren) { 9 | const { style, ...props } = inProps; 10 | const theme = useTheme(); 11 | 12 | return ( 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/widgets/ErrorDialog.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { Paragraph } from "react-native-paper"; 6 | 7 | import ConfirmationDialog from "./ConfirmationDialog"; 8 | 9 | interface PropsType { 10 | title?: string; 11 | error?: string; 12 | onOk?: () => void | Promise; 13 | labelOk?: string; 14 | loadingText?: string; 15 | } 16 | 17 | export default React.memo(function ErrorDialog(props: PropsType) { 18 | return ( 19 | 27 | 28 | {props.error} 29 | 30 | 31 | ); 32 | }); 33 | -------------------------------------------------------------------------------- /src/widgets/ErrorOrLoadingDialog.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | 6 | import ErrorDialog from "./ErrorDialog"; 7 | import ConfirmationDialog from "./ConfirmationDialog"; 8 | 9 | interface PropsType { 10 | loading?: boolean; 11 | error?: Error; 12 | onDismiss: () => void; 13 | loadingText?: string; 14 | } 15 | 16 | export default React.memo(function ErrorOrLoadingDialog(props: PropsType) { 17 | if (props.error) { 18 | return ( 19 | 23 | ); 24 | } else if (props.loading) { 25 | return ( 26 | 33 | 34 | 35 | ); 36 | } 37 | 38 | return null; 39 | }); 40 | -------------------------------------------------------------------------------- /src/widgets/ExternalLink.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { ViewProps, Linking } from "react-native"; 6 | import { Button } from "react-native-paper"; 7 | 8 | type PropsType = { 9 | href: string; 10 | } & ViewProps; 11 | 12 | class ExternalLink extends React.PureComponent { 13 | public render() { 14 | const { href, children, ...props } = this.props; 15 | return ( 16 | 23 | ); 24 | } 25 | } 26 | 27 | export default ExternalLink; 28 | -------------------------------------------------------------------------------- /src/widgets/LoadingIndicator.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { View } from "react-native"; 6 | import { ActivityIndicator, Text } from "react-native-paper"; 7 | 8 | import Container from "./Container"; 9 | export default function LoadingIndicator(_props: any) { 10 | const { style, status, notice, ...props } = _props; 11 | return ( 12 | 13 | 14 | 15 | {status && {status}} 16 | 17 | {notice && {notice}} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/widgets/Markdown.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { Linking } from "react-native"; 6 | import { useTheme } from "react-native-paper"; 7 | import MarkdownDisplay from "react-native-markdown-display"; 8 | 9 | const Markdown = React.memo(function _Markdown(props: { content: string }) { 10 | const theme = useTheme(); 11 | 12 | const blockBackgroundColor = (theme.dark) ? "#555555" : "#cccccc"; 13 | 14 | return ( 15 | { 27 | Linking.openURL(url); 28 | return true; 29 | }} 30 | > 31 | {props.content} 32 | 33 | ); 34 | }); 35 | 36 | export default Markdown; 37 | 38 | -------------------------------------------------------------------------------- /src/widgets/PasswordInput.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { View } from "react-native"; 6 | import { IconButton } from "react-native-paper"; 7 | 8 | import TextInput from "./TextInput"; 9 | 10 | const PasswordInput = React.memo(React.forwardRef(function _PasswordInput(inProps: React.ComponentPropsWithoutRef, ref) { 11 | const [isPassword, setIsPassword] = React.useState(true); 12 | const { 13 | style, 14 | ...props 15 | } = inProps; 16 | 17 | return ( 18 | 19 | 26 | setIsPassword(!isPassword)} 31 | /> 32 | 33 | ); 34 | })); 35 | 36 | export default PasswordInput; 37 | -------------------------------------------------------------------------------- /src/widgets/PrettyFingerprint.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import sjcl from "sjcl"; 6 | 7 | import { Paragraph } from "react-native-paper"; 8 | 9 | import { byte, base64 } from "etesync"; 10 | 11 | function byteArray4ToNumber(bytes: byte[], offset: number) { 12 | // tslint:disable:no-bitwise 13 | return ( 14 | ((bytes[offset + 0] & 0xff) * (1 << 24)) + 15 | ((bytes[offset + 1] & 0xff) * (1 << 16)) + 16 | ((bytes[offset + 2] & 0xff) * (1 << 8)) + 17 | ((bytes[offset + 3] & 0xff)) 18 | ); 19 | } 20 | 21 | function getEncodedChunk(publicKey: byte[], offset: number) { 22 | const chunk = byteArray4ToNumber(publicKey, offset) % 100000; 23 | return chunk.toString().padStart(5, "0"); 24 | } 25 | 26 | interface PropsType { 27 | publicKey: base64; 28 | } 29 | 30 | class PrettyFingerprint extends React.PureComponent { 31 | public render() { 32 | const fingerprint = sjcl.codec.bytes.fromBits( 33 | sjcl.hash.sha256.hash(sjcl.codec.base64.toBits(this.props.publicKey)) 34 | ); 35 | 36 | const spacing = " "; 37 | const prettyPublicKey = 38 | getEncodedChunk(fingerprint, 0) + spacing + 39 | getEncodedChunk(fingerprint, 4) + spacing + 40 | getEncodedChunk(fingerprint, 8) + spacing + 41 | getEncodedChunk(fingerprint, 12) + "\n" + 42 | getEncodedChunk(fingerprint, 16) + spacing + 43 | getEncodedChunk(fingerprint, 20) + spacing + 44 | getEncodedChunk(fingerprint, 24) + spacing + 45 | getEncodedChunk(fingerprint, 28); 46 | 47 | return ( 48 | {prettyPublicKey} 49 | ); 50 | } 51 | } 52 | 53 | export default PrettyFingerprint; 54 | -------------------------------------------------------------------------------- /src/widgets/PrettyFingerprintEb.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2017 EteSync Authors 2 | // SPDX-License-Identifier: AGPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { TextProps } from "react-native"; 6 | import * as Etebase from "etebase"; 7 | 8 | import { Paragraph } from "react-native-paper"; 9 | 10 | interface PropsType extends TextProps { 11 | publicKey: Uint8Array; 12 | } 13 | 14 | export default function PrettyFingerprint(props: PropsType) { 15 | const prettyFingerprint = Etebase.getPrettyFingerprint(props.publicKey); 16 | 17 | return ( 18 | {prettyFingerprint} 19 | ); 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/widgets/Row.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { StyleSheet, ViewProps, View } from "react-native"; 6 | 7 | class Row extends React.Component { 8 | public render() { 9 | const { children, style } = this.props; 10 | 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | } 17 | } 18 | 19 | const styles = StyleSheet.create({ 20 | row: { 21 | flexDirection: "row", 22 | }, 23 | }); 24 | 25 | export default Row; 26 | -------------------------------------------------------------------------------- /src/widgets/ScrollView.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { ScrollViewProps, ScrollView as NativeScrollView } from "react-native"; 6 | import { useTheme } from "react-native-paper"; 7 | import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view"; 8 | 9 | export default function ScrollView(inProps: React.PropsWithChildren & { keyboardAware?: boolean }) { 10 | const { keyboardAware, style, ...props } = inProps; 11 | const theme = useTheme(); 12 | 13 | const Scroller = (keyboardAware) ? KeyboardAwareScrollView : NativeScrollView; 14 | 15 | return ( 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/widgets/Select.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { ViewProps } from "react-native"; 6 | import { Menu } from "react-native-paper"; 7 | 8 | interface PropsType extends ViewProps { 9 | visible: boolean; 10 | anchor: React.ReactNode; 11 | options: T[]; 12 | noneString?: string; 13 | titleAccossor?: (item: T) => string; 14 | onChange: (item: T | null) => void; 15 | onDismiss: () => void; 16 | } 17 | 18 | export default function Select(inProps: React.PropsWithChildren>) { 19 | const { visible, anchor, options, onDismiss, noneString, titleAccossor, onChange, ...props } = inProps; 20 | 21 | const getTitle = titleAccossor ?? ((item: T) => item); 22 | 23 | return ( 24 | 30 | {noneString && ( 31 | onChange(null)} title={noneString} /> 32 | )} 33 | {options.map((item, idx) => ( 34 | onChange(item)} title={getTitle(item)} /> 35 | ))} 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/widgets/Small.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | 6 | import { Text } from "react-native"; 7 | 8 | export default React.memo(function Small(props: React.PropsWithChildren<{}>) { 9 | return ( 10 | {props.children} 11 | ); 12 | }); 13 | -------------------------------------------------------------------------------- /src/widgets/TextInput.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { TextInput as PaperTextInput, useTheme } from "react-native-paper"; 6 | 7 | export default React.memo(React.forwardRef(function PasswordInput(inProps: React.ComponentPropsWithoutRef, ref) { 8 | const theme = useTheme(); 9 | const { 10 | style, 11 | ...props 12 | } = inProps; 13 | 14 | return ( 15 | 21 | ); 22 | })); 23 | -------------------------------------------------------------------------------- /src/widgets/Typography.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { Subheading as PaperSubheading, Title as PaperTitle, Headline as PaperHeadline } from "react-native-paper"; 6 | 7 | export const Subheading = React.memo(function Subheading(props: React.ComponentProps) { 8 | return ( 9 | 14 | ); 15 | }); 16 | 17 | export const Title = React.memo(function Subheading(props: React.ComponentProps) { 18 | return ( 19 | 24 | ); 25 | }); 26 | 27 | export const Headline = React.memo(function Subheading(props: React.ComponentProps) { 28 | return ( 29 | 34 | ); 35 | }); 36 | -------------------------------------------------------------------------------- /src/widgets/Wizard.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { ViewProps, View } from "react-native"; 6 | import { Button } from "react-native-paper"; 7 | 8 | import Container from "./Container"; 9 | import ScrollView from "./ScrollView"; 10 | 11 | export interface PagePropsType { 12 | prev?: () => void; 13 | next?: () => void; 14 | currentPage: number; 15 | totalPages: number; 16 | } 17 | 18 | export function WizardNavigationBar(props: PagePropsType) { 19 | const first = props.currentPage === 0; 20 | const last = props.currentPage === props.totalPages - 1; 21 | 22 | return ( 23 | 24 | 32 | 39 | 40 | ); 41 | } 42 | 43 | interface PropsType extends ViewProps { 44 | pages: ((props: PagePropsType) => React.ReactNode)[]; 45 | onFinish: () => void; 46 | } 47 | 48 | export default function Wizard(inProps: PropsType) { 49 | const [currentPage, setCurrentPage] = React.useState(0); 50 | const { pages, onFinish, ...props } = inProps; 51 | 52 | const Content = pages[currentPage]; 53 | 54 | const first = currentPage === 0; 55 | const last = currentPage === pages.length - 1; 56 | const prev = !first ? () => setCurrentPage(currentPage - 1) : undefined; 57 | const next = !last ? () => setCurrentPage(currentPage + 1) : onFinish; 58 | 59 | return ( 60 | 61 | 62 | {Content({ prev, next, currentPage, totalPages: pages.length })} 63 | 64 | 65 | ); 66 | } 67 | 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "jsx": "react-native", 5 | "downlevelIteration": true, 6 | "noUnusedLocals": true, 7 | "moduleResolution": "node", 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": false, 12 | "noImplicitReturns": true, 13 | "noImplicitAny": true, 14 | "noImplicitThis": true, 15 | "noUnusedParameters": false, 16 | "strictNullChecks": true, 17 | "strictBindCallApply": true, 18 | "strictFunctionTypes": true, 19 | "suppressImplicitAnyIndexErrors": true, 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "noEmit": true 23 | } 24 | } 25 | --------------------------------------------------------------------------------