├── .watchmanconfig ├── AUTHORS ├── assets ├── icon.png └── splash.png ├── src ├── images │ └── icon.png ├── types │ └── redux-persist.d.ts ├── sync │ ├── index.ts │ ├── SyncManagerTaskList.ts │ └── legacy │ │ └── SyncManagerTaskList.ts ├── widgets │ ├── Small.tsx │ ├── Container.tsx │ ├── Row.tsx │ ├── PrettyFingerprintEb.tsx │ ├── TextInput.tsx │ ├── ColorBox.tsx │ ├── ExternalLink.tsx │ ├── ScrollView.tsx │ ├── LoadingIndicator.tsx │ ├── ErrorDialog.tsx │ ├── Typography.tsx │ ├── Checkbox.tsx │ ├── ErrorOrLoadingDialog.tsx │ ├── PasswordInput.tsx │ ├── Select.tsx │ ├── Markdown.tsx │ ├── PrettyFingerprint.tsx │ ├── Wizard.tsx │ ├── ColorPicker.tsx │ ├── Alert.tsx │ └── ConfirmationDialog.tsx ├── helpers.test.tsx ├── JournalItemHeader.tsx ├── credentials.tsx ├── store │ ├── promise-middleware.ts │ ├── index.test.ts │ └── index.ts ├── constants │ └── index.ts ├── index.tsx ├── DebugLogsScreen.tsx ├── SettingsGate.tsx ├── login │ └── index.tsx ├── etesync-helpers.ts ├── SyncGate.tsx ├── HomeScreen.tsx ├── components │ ├── EncryptionLoginForm.tsx │ ├── WebviewKeygen.tsx │ └── JournalListScreenEb.tsx ├── CollectionItemEvent.tsx ├── JournalItemEvent.tsx ├── LegacyHomeScreen.tsx ├── CollectionItemTask.tsx ├── JournalItemTask.tsx ├── App.tsx ├── logging.ts ├── AboutScreen.tsx ├── CollectionItemScreen.tsx ├── EteSyncNative.ts ├── Permissions.tsx ├── JournalItemScreen.tsx ├── SyncHandler.tsx ├── CollectionMemberAddDialog.tsx ├── InvitationsScreen.tsx ├── CollectionItemContact.tsx └── JournalItemContact.tsx ├── App.ts ├── android ├── debug.keystore ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── app │ ├── src │ │ └── main │ │ │ ├── res │ │ │ ├── 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 │ │ │ ├── drawable-xxxhdpi │ │ │ │ ├── pin_white.png │ │ │ │ ├── big_logo_dark.png │ │ │ │ ├── pin_white_fade.png │ │ │ │ ├── big_logo_filled.png │ │ │ │ ├── notification_icon.png │ │ │ │ ├── big_logo_new_filled.png │ │ │ │ ├── ic_home_white_36dp.png │ │ │ │ ├── ic_logo_white_32dp.png │ │ │ │ ├── ic_share_white_36dp.png │ │ │ │ ├── big_logo_dark_filled.png │ │ │ │ ├── ic_refresh_white_36dp.png │ │ │ │ ├── ic_arrow_back_white_36dp.png │ │ │ │ ├── shell_notification_icon.png │ │ │ │ └── shell_launch_background_image.png │ │ │ ├── drawable-hdpi │ │ │ │ ├── ic_home_white_36dp.png │ │ │ │ ├── ic_logo_white_32dp.png │ │ │ │ ├── ic_share_white_36dp.png │ │ │ │ ├── ic_refresh_white_36dp.png │ │ │ │ └── ic_arrow_back_white_36dp.png │ │ │ ├── drawable-mdpi │ │ │ │ ├── ic_home_white_36dp.png │ │ │ │ ├── ic_logo_white_32dp.png │ │ │ │ ├── ic_share_white_36dp.png │ │ │ │ ├── ic_refresh_white_36dp.png │ │ │ │ └── ic_arrow_back_white_36dp.png │ │ │ ├── drawable-xhdpi │ │ │ │ ├── ic_home_white_36dp.png │ │ │ │ ├── ic_logo_white_32dp.png │ │ │ │ ├── ic_share_white_36dp.png │ │ │ │ ├── ic_refresh_white_36dp.png │ │ │ │ └── ic_arrow_back_white_36dp.png │ │ │ ├── drawable-xxhdpi │ │ │ │ ├── ic_home_white_36dp.png │ │ │ │ ├── ic_logo_white_32dp.png │ │ │ │ ├── ic_share_white_36dp.png │ │ │ │ ├── ic_refresh_white_36dp.png │ │ │ │ └── ic_arrow_back_white_36dp.png │ │ │ ├── drawable │ │ │ │ └── splash_background.xml │ │ │ ├── values │ │ │ │ ├── dimens.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ ├── values-w820dp │ │ │ │ └── dimens.xml │ │ │ └── layout │ │ │ │ ├── error_activity_new.xml │ │ │ │ ├── error_console_list_item.xml │ │ │ │ ├── notification_shell_app.xml │ │ │ │ ├── error_console_fragment.xml │ │ │ │ ├── notification.xml │ │ │ │ └── error_fragment.xml │ │ │ ├── java │ │ │ └── host │ │ │ │ └── exp │ │ │ │ └── exponent │ │ │ │ ├── generated │ │ │ │ ├── DetachBuildConstants.java │ │ │ │ └── AppConstants.java │ │ │ │ ├── MainActivity.java │ │ │ │ └── MainApplication.java │ │ │ └── assets │ │ │ └── kernel-manifest.json │ └── expo.gradle ├── gradle.properties ├── settings.gradle └── build.gradle ├── ios ├── etesync │ ├── Supporting │ │ ├── sdkVersions.json │ │ ├── launch_icon.png │ │ ├── launch_background_image.png │ │ ├── EXShell.json │ │ ├── main.m │ │ ├── EXShell.plist │ │ ├── EXSDKVersions.plist │ │ ├── Info.plist │ │ └── LaunchScreen.xib │ ├── 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~ipad.png │ │ │ ├── AppIcon76x76@2x~ipad.png │ │ │ ├── AppIcon83.5x83.5@2x~ipad.png │ │ │ └── Contents.json │ ├── AppDelegate.h │ ├── etesync.entitlements │ ├── modules │ │ ├── etesync-Bridging-Header.h │ │ ├── EteEXCalendar.h │ │ ├── EtesyncNativeBridge.m │ │ └── EtesyncNativeTest.swift │ └── AppDelegate.m ├── etesync.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── etesync.xcscheme ├── etesync.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── EteSyncTests │ ├── Info.plist │ └── EteSyncTests.swift └── Podfile ├── .github └── FUNDING.yml ├── babel.config.js ├── .expo-shared └── assets.json ├── deploy_dist.sh ├── tsconfig.json ├── shim.js ├── README.md ├── license-gen.js ├── app.json ├── etesync.mobileconfig ├── package.json └── .eslintrc.js /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Tom Hacohen 2 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/assets/splash.png -------------------------------------------------------------------------------- /src/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/src/images/icon.png -------------------------------------------------------------------------------- /App.ts: -------------------------------------------------------------------------------- 1 | import './shim'; 2 | import Index from './src/index'; 3 | export default Index; 4 | -------------------------------------------------------------------------------- /android/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/debug.keystore -------------------------------------------------------------------------------- /ios/etesync/Supporting/sdkVersions.json: -------------------------------------------------------------------------------- 1 | {"sdkVersions":["33.0.0","34.0.0","35.0.0","36.0.0"]} -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: etesync 2 | custom: https://www.etesync.com/contribute/#donate 3 | -------------------------------------------------------------------------------- /ios/etesync/Supporting/launch_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/ios/etesync/Supporting/launch_icon.png -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/dev_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/mipmap-hdpi/dev_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/dev_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/mipmap-mdpi/dev_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/dev_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/mipmap-xhdpi/dev_icon.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /ios/etesync/Supporting/launch_background_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/ios/etesync/Supporting/launch_background_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/dev_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/mipmap-xxhdpi/dev_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/dev_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/mipmap-xxxhdpi/dev_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/pin_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xxxhdpi/pin_white.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/big_logo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xxxhdpi/big_logo_dark.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/pin_white_fade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xxxhdpi/pin_white_fade.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_home_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/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/HEAD/android/app/src/main/res/drawable-hdpi/ic_logo_white_32dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_share_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-hdpi/ic_share_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_home_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/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/HEAD/android/app/src/main/res/drawable-mdpi/ic_logo_white_32dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_share_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-mdpi/ic_share_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_home_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/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/HEAD/android/app/src/main/res/drawable-xhdpi/ic_logo_white_32dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_share_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xhdpi/ic_share_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_home_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/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/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_logo_white_32dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/big_logo_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xxxhdpi/big_logo_filled.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_refresh_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-hdpi/ic_refresh_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_refresh_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-mdpi/ic_refresh_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_refresh_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xhdpi/ic_refresh_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_share_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_share_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/big_logo_new_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xxxhdpi/big_logo_new_filled.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_home_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/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/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_logo_white_32dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_share_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_share_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_arrow_back_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-hdpi/ic_arrow_back_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_arrow_back_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-mdpi/ic_arrow_back_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_arrow_back_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xhdpi/ic_arrow_back_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_refresh_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_refresh_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/big_logo_dark_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xxxhdpi/big_logo_dark_filled.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_refresh_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_refresh_white_36dp.png -------------------------------------------------------------------------------- /ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon1024x1024.png -------------------------------------------------------------------------------- /ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon20x20@2x.png -------------------------------------------------------------------------------- /ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon20x20@3x.png -------------------------------------------------------------------------------- /ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@2x.png -------------------------------------------------------------------------------- /ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon29x29@3x.png -------------------------------------------------------------------------------- /ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@2x.png -------------------------------------------------------------------------------- /ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon40x40@3x.png -------------------------------------------------------------------------------- /ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@2x.png -------------------------------------------------------------------------------- /ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon60x60@3x.png -------------------------------------------------------------------------------- /ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon76x76~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon76x76~ipad.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_arrow_back_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_white_36dp.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/shell_notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xxxhdpi/shell_notification_icon.png -------------------------------------------------------------------------------- /ios/etesync/Supporting/EXShell.json: -------------------------------------------------------------------------------- 1 | {"isShell":true,"manifestUrl":"https://expo.etesync.com/release/5/ios-index.json","releaseChannel":"default","isManifestVerificationBypassed":true} 2 | -------------------------------------------------------------------------------- /ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon76x76@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon76x76@2x~ipad.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/shell_launch_background_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/android/app/src/main/res/drawable-xxxhdpi/shell_launch_background_image.png -------------------------------------------------------------------------------- /ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon83.5x83.5@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etesync/ios/HEAD/ios/etesync/Assets.xcassets/AppIcon.appiconset/AppIcon83.5x83.5@2x~ipad.png -------------------------------------------------------------------------------- /ios/etesync.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/splash_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/etesync/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // Copyright 2015-present 650 Industries. All rights reserved. 2 | 3 | #import 4 | #import 5 | 6 | @interface AppDelegate : EXStandaloneAppDelegate 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "bcbfadc7151fde99a5ae3871cdabbd22f007ea6cbaee2be3b3d87dc27101c53a": true, 3 | "af0ce96885147c05d7b5bf4162cce9dc050bdbf338b0bae9a3ec86b4f9a833a9": true, 4 | "baddda505ed7aa3bd17ad9541c791e7834f15e9433bb3b044683427021e414e4": true 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Sep 19 13:00:59 CEST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.3-all.zip 7 | -------------------------------------------------------------------------------- /ios/etesync/etesync.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.contacts.notes 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 16dp 6 | 7 | -------------------------------------------------------------------------------- /ios/etesync/modules/etesync-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import 6 | #import 7 | #import 8 | #import "EteEXCalendar.h" 9 | -------------------------------------------------------------------------------- /ios/etesync.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/etesync.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel=true 2 | org.gradle.daemon=true 3 | org.gradle.jvmargs=-Xmx9216M -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 4 | org.gradle.configureondemand=true 5 | org.gradle.internal.repository.initial.backoff=1000 6 | android.useAndroidX=true 7 | android.enableJetifier=true 8 | -------------------------------------------------------------------------------- /ios/etesync/Supporting/main.m: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 650 Industries, Inc. All rights reserved. 2 | 3 | #import 4 | #import "AppDelegate.h" 5 | 6 | int main(int argc, char * argv[]) { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | include ':react-native-rsa-native' 3 | project(':react-native-rsa-native').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-rsa-native/android') 4 | 5 | 6 | // Import gradle helpers for unimodules. 7 | apply from: '../node_modules/react-native-unimodules/gradle.groovy' 8 | 9 | // Include unimodules. 10 | includeUnimodulesProjects( 11 | ) 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/error_activity_new.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | -------------------------------------------------------------------------------- /ios/etesync/modules/EteEXCalendar.h: -------------------------------------------------------------------------------- 1 | // Copyright 2015-present 650 Industries. All rights reserved. 2 | 3 | #import 4 | 5 | @interface EteEXCalendar : NSObject 6 | 7 | - (EKEvent *)deserializeEvent:(EKEvent *)calendarEvent details:(NSDictionary *)details reject:(RCTPromiseRejectBlock)reject; 8 | - (EKReminder *)deserializeReminder:(EKReminder *)reminder details:(NSDictionary *)details reject:(RCTPromiseRejectBlock)reject; 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /ios/etesync/Supporting/EXShell.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | isManifestVerificationBypassed 6 | 7 | isShell 8 | 9 | manifestUrl 10 | https://expo.etesync.com/release/5/ios-index.json 11 | releaseChannel 12 | default 13 | 14 | 15 | -------------------------------------------------------------------------------- /ios/etesync/Supporting/EXSDKVersions.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | detachedNativeVersions 6 | 7 | kernel 8 | 36.0.0 9 | shell 10 | 36.0.0 11 | 12 | sdkVersions 13 | 14 | 36.0.0 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /deploy_dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SSH_HOST=expo.etesync.com 6 | SSH_PORT=22 7 | SSH_USER=etesync 8 | SSH_TARGET_DIR=sites/expo.etesync.com 9 | 10 | OUTPUTDIR='dist' 11 | 12 | PUBLIC_URL=https://expo.etesync.com 13 | DEPLOY_PATH='release' 14 | # DEPLOY_PATH='test' 15 | 16 | APP_VERSION=5 17 | 18 | yarn lint --max-warnings 0 19 | yarn tsc 20 | 21 | rm -rf "$OUTPUTDIR" 22 | yarn run expo export --dump-sourcemap --public-url ${PUBLIC_URL}/${DEPLOY_PATH}/${APP_VERSION} 23 | rsync -e "ssh -p ${SSH_PORT}" -P --delete --exclude '*android*' -rvc -zz ${OUTPUTDIR}/ ${SSH_USER}@${SSH_HOST}:${SSH_TARGET_DIR}/${DEPLOY_PATH}/${APP_VERSION} 24 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #1b73b4 4 | #011A2D 5 | #1b73b4 6 | #FFFFFF 7 | #FFFFFF 8 | #FFFFFF 9 | #FF0000 10 | #66FFFFFF 11 | #F6F6F7 12 | #fdf6df 13 | #FFFFFF 14 | 15 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /ios/EteSyncTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /shim.js: -------------------------------------------------------------------------------- 1 | if (typeof __dirname === 'undefined') global.__dirname = '/' 2 | if (typeof __filename === 'undefined') global.__filename = '' 3 | if (typeof process === 'undefined') { 4 | global.process = require('process') 5 | } else { 6 | const bProcess = require('process') 7 | for (var p in bProcess) { 8 | if (!(p in process)) { 9 | process[p] = bProcess[p] 10 | } 11 | } 12 | } 13 | 14 | process.browser = false 15 | if (typeof Buffer === 'undefined') global.Buffer = require('buffer').Buffer 16 | 17 | // global.location = global.location || { port: 80 } 18 | const isDev = typeof __DEV__ === 'boolean' && __DEV__ 19 | process.env['NODE_ENV'] = isDev ? 'development' : 'production' 20 | if (typeof localStorage !== 'undefined') { 21 | localStorage.debug = isDev ? '*' : '' 22 | } 23 | 24 | // If using the crypto shim, uncomment the following line to ensure 25 | // crypto is loaded first, so it can populate global.crypto 26 | // require('crypto') 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | EteSync 3 | host.exp.exponent.SharedPreferences 4 | Something went wrong. 5 | Sorry about that. You can go back to Expo home or try to reload the project. 6 | Sorry about that. Press the reload button to try again. 7 | Unable to load Experience. 8 | Uncaught Error: %s 9 | View error log 10 | Default 11 | Experience notifications 12 | Persistent notifications that provide debug info about open experiences 13 | 14 | -------------------------------------------------------------------------------- /ios/EteSyncTests/EteSyncTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EteSyncTests.swift 3 | // EteSyncTests 4 | // 5 | // Created by Me Me on 26/12/2019. 6 | // Copyright © 2019 650 Industries, Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class EteSyncTests: XCTestCase { 12 | 13 | override func setUp() { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDown() { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /src/DebugLogsScreen.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, Clipboard } from "react-native"; 6 | import { Button, Text } from "react-native-paper"; 7 | 8 | import ScrollView from "./widgets/ScrollView"; 9 | import Container from "./widgets/Container"; 10 | import { getLogs, clearLogs } from "./logging"; 11 | 12 | export default function DebugLogsScreen() { 13 | const [logs, setLogs] = React.useState(); 14 | 15 | React.useEffect(() => { 16 | getLogs().then((value) => setLogs(value.join("\n"))); 17 | }, []); 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {(logs) ? logs : "No logs found"} 29 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /license-gen.js: -------------------------------------------------------------------------------- 1 | // Running: node license-gen.js > licenses.json 2 | 3 | const checker = require('license-checker'); 4 | 5 | const packageJson = require('./package.json'); 6 | 7 | const dependencies = packageJson.dependencies; 8 | const devDependencies = packageJson.devDependencies; 9 | 10 | function filterProperties(pkg) { 11 | const allowed = ['licenses', 'repository', 'url', 'publisher']; 12 | Object.keys(pkg).forEach((key) => { 13 | if (!allowed.includes(key)) { 14 | delete pkg[key]; 15 | } 16 | }); 17 | 18 | return pkg; 19 | } 20 | 21 | checker.init({ 22 | start: '.', 23 | }, function (err, packages) { 24 | const output = { 25 | dependencies: {}, 26 | devDependencies: {}, 27 | }; 28 | 29 | if (err) { 30 | console.error(err); 31 | process.exit(1); 32 | } else { 33 | Object.keys(packages).forEach((pkg) => { 34 | const pkgName = pkg.replace(/@[^@]+$/, ''); 35 | if (dependencies[pkgName]) { 36 | output.dependencies[pkgName] = filterProperties(packages[pkg]); 37 | } 38 | if (devDependencies[pkgName]) { 39 | output.devDependencies[pkgName] = filterProperties(packages[pkg]); 40 | } 41 | }); 42 | 43 | console.log(JSON.stringify(output, null, 2)); 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/CollectionItemEvent.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 Color from "color"; 7 | 8 | import { Text } from "react-native-paper"; 9 | 10 | import { DecryptedCollection, DecryptedItem } from "./store"; 11 | 12 | import Container from "./widgets/Container"; 13 | import Small from "./widgets/Small"; 14 | 15 | import { EventType } from "./pim-types"; 16 | import { formatDateRange, formatOurTimezoneOffset } from "./helpers"; 17 | 18 | import JournalItemHeader from "./JournalItemHeader"; 19 | 20 | interface PropsType { 21 | collection: DecryptedCollection; 22 | item: DecryptedItem; 23 | } 24 | 25 | export default React.memo(function CollectionItemEvent(props: PropsType) { 26 | const entry = props.item; 27 | const event = EventType.parse(entry.content); 28 | 29 | const timezone = event.timezone; 30 | 31 | const backgroundColor = props.collection.meta.color; 32 | const foregroundColor = Color(backgroundColor).isLight() ? "black" : "white"; 33 | 34 | return ( 35 | <> 36 | 37 | {formatDateRange(event.startDate, event.endDate)} {timezone && ({formatOurTimezoneOffset()})} 38 | {event.location} 39 | 40 | 41 | {event.description} 42 | {(event.attendees.length > 0) && ( 43 | Attendees: {event.attendees.map((x) => (x.getFirstValue())).join(", ")})} 44 | 45 | 46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 18 | 19 | 22 | 23 | 29 | 30 | 35 | 36 | 47 | 48 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/notification_shell_app.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 17 | 18 | 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 | -------------------------------------------------------------------------------- /ios/etesync/modules/EtesyncNativeBridge.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface RCT_EXTERN_MODULE(EteSyncNative, NSObject) 4 | 5 | RCT_EXTERN_METHOD(hashEvent:(NSString *)eventId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) 6 | RCT_EXTERN_METHOD(calculateHashesForEvents:(NSString *)calendarId from:(nonnull NSNumber *)from to:(nonnull NSNumber *)to resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) 7 | RCT_EXTERN_METHOD(processEventsChanges:(NSString *)containerId changes: (NSArray *)changes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) 8 | 9 | RCT_EXTERN_METHOD(hashReminder:(NSString *)reminderId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) 10 | RCT_EXTERN_METHOD(calculateHashesForReminders:(NSString *)calendarId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) 11 | RCT_EXTERN_METHOD(processRemindersChanges:(NSString *)containerId changes: (NSArray *)changes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) 12 | 13 | RCT_EXTERN_METHOD(hashContact:(NSString *)contactId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) 14 | RCT_EXTERN_METHOD(calculateHashesForContacts:(NSString *)containerId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) 15 | RCT_EXTERN_METHOD(processContactsChanges:(NSString *)containerId groupId:(NSString *)groupId changes: (NSArray *)changes resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) 16 | RCT_EXTERN_METHOD(deleteContactGroupAndMembers:(NSString *)groupId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) 17 | RCT_EXTERN_METHOD(getContainers:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) 18 | 19 | RCT_EXTERN_METHOD(beginBackgroundTask:(NSString *)name resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) 20 | RCT_EXTERN_METHOD(endBackgroundTask:(nonnull NSNumber *)taskId) 21 | 22 | RCT_EXTERN_METHOD(playground:(NSDictionary *)dictionary) 23 | @end 24 | -------------------------------------------------------------------------------- /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"} -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2019 EteSync Authors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import * as React from "react"; 5 | import { StatusBar } from "react-native"; 6 | import { DarkTheme, DefaultTheme, Provider as PaperProvider, Theme, Colors } from "react-native-paper"; 7 | 8 | import { AppearanceProvider, useColorScheme } from "react-native-appearance"; 9 | 10 | import { NavigationContainer } from "@react-navigation/native"; 11 | import { createDrawerNavigator } from "@react-navigation/drawer"; 12 | import RootNavigator from "./RootNavigator"; 13 | 14 | import ErrorBoundary from "./ErrorBoundary"; 15 | import Drawer from "./Drawer"; 16 | import SettingsGate from "./SettingsGate"; 17 | 18 | import "react-native-gesture-handler"; 19 | import { enableScreens } from "react-native-screens"; 20 | enableScreens(); 21 | 22 | const DrawerNavigation = createDrawerNavigator(); 23 | 24 | function InnerApp() { 25 | const colorScheme = useColorScheme(); 26 | 27 | const baseTheme = (colorScheme === "dark") ? DarkTheme : DefaultTheme; 28 | 29 | const theme: Theme = { 30 | ...baseTheme, 31 | mode: "exact", 32 | colors: { 33 | ...baseTheme.colors, 34 | primary: Colors.amber500, 35 | accent: Colors.lightBlueA700, // Not the real etesync theme but better for accessibility 36 | }, 37 | }; 38 | 39 | return ( 40 | 41 | 42 | 43 | 44 | }> 45 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | 54 | class App extends React.Component { 55 | public render() { 56 | return ( 57 | 58 | 59 | 60 | 61 | ); 62 | } 63 | } 64 | 65 | export default App; 66 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "EteSync", 4 | "description": "Secure, end-to-end encrypted, and privacy respecting sync for your contacts, calendars and tasks.", 5 | "slug": "etesync-ios", 6 | "privacy": "public", 7 | "sdkVersion": "36.0.0", 8 | "platforms": [ 9 | "ios" 10 | ], 11 | "version": "1.6.1", 12 | "githubUrl": "https://github.com/etesync/ios", 13 | "icon": "./assets/icon.png", 14 | "splash": { 15 | "image": "./assets/splash.png", 16 | "resizeMode": "contain", 17 | "backgroundColor": "#fdf6df" 18 | }, 19 | "assetBundlePatterns": [ 20 | "**/*" 21 | ], 22 | "ios": { 23 | "bundleIdentifier": "com.etesync.ios", 24 | "buildNumber": "1.6.101", 25 | "userInterfaceStyle": "automatic", 26 | "supportsTablet": true, 27 | "infoPlist": { 28 | "NSContactsUsageDescription": "Access to your contacts is needed in order to load and save EteSync contacts.", 29 | "NSCalendarsUsageDescription": "Access to your calendars is needed in order to load and save EteSync calendars.", 30 | "NSRemindersUsageDescription": "Access to your reminders is needed in order to load and save EteSync reminders.", 31 | "UIBackgroundModes": [ 32 | "fetch" 33 | ] 34 | }, 35 | "config": { 36 | "usesNonExemptEncryption": false 37 | }, 38 | "publishBundlePath": "ios/etesync/Supporting/shell-app.bundle", 39 | "publishManifestPath": "ios/etesync/Supporting/shell-app-manifest.json" 40 | }, 41 | "isDetached": true, 42 | "detach": { 43 | "iosExpoViewUrl": "https://s3.amazonaws.com/exp-exponent-view-code/ios-v2.14.2-sdk36.0.0-e055f8a6-47bf-455c-9f14-d5a631ea2dd3.tar.gz", 44 | "androidExpoViewUrl": "https://s3.amazonaws.com/exp-exponent-view-code/android-v2.14.0-sdk36.0.0-fb8689e6-fd42-4b18-b01f-ac2ef3bc8681.tar.gz" 45 | }, 46 | "scheme": "exp2904f03229f24a3ca9b3fcd8485120ca", 47 | "android": { 48 | "package": "com.etesync.android", 49 | "publishBundlePath": "android/app/src/main/assets/shell-app.bundle", 50 | "publishManifestPath": "android/app/src/main/assets/shell-app-manifest.json" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ios/etesync/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // Copyright 2015-present 650 Industries. All rights reserved. 2 | 3 | #import "AppDelegate.h" 4 | 5 | @implementation AppDelegate 6 | 7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 8 | { 9 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 10 | } 11 | 12 | - (void)applicationWillEnterForeground:(UIApplication *)application 13 | { 14 | [super applicationWillEnterForeground:application]; 15 | } 16 | 17 | #pragma mark - Background Fetch 18 | 19 | - (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler 20 | { 21 | [super application:application performFetchWithCompletionHandler:completionHandler]; 22 | } 23 | 24 | #pragma mark - Handling URLs 25 | 26 | - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options 27 | { 28 | return [super application:app openURL:url options:options]; 29 | } 30 | 31 | - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray> * _Nullable))restorationHandler 32 | { 33 | return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; 34 | } 35 | 36 | #pragma mark - Notifications 37 | 38 | - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)token 39 | { 40 | [super application:application didRegisterForRemoteNotificationsWithDeviceToken:token]; 41 | } 42 | 43 | - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)err 44 | { 45 | [super application:application didFailToRegisterForRemoteNotificationsWithError:err]; 46 | } 47 | 48 | - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler 49 | { 50 | [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; 51 | } 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /etesync.mobileconfig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PayloadVersion 6 | 1 7 | 8 | PayloadUUID 9 | 83f2618b-c486-4c1b-8a6d-9c285d72cf98 10 | 11 | PayloadType 12 | Configuration 13 | 14 | PayloadIdentifier 15 | com.etesync.ios 16 | 17 | PayloadDisplayName 18 | EteSync Accounts 19 | 20 | PayloadOrganization 21 | EteSync 22 | 23 | Label 24 | Account configuration for the EteSync app 25 | 26 | PayloadContent 27 | 28 | 29 | CardDAVAccountDescription 30 | etesync 31 | 32 | CardDAVHostName 33 | 127.0.0.1 34 | 35 | CardDAVUsername 36 | etesync 37 | 38 | CardDAVPassword 39 | etesync 40 | 41 | PayloadDescription 42 | Configures CardDAV account 43 | 44 | PayloadIdentifier 45 | com.etesync.ios.carddav 46 | 47 | PayloadType 48 | com.apple.carddav.account 49 | 50 | PayloadUUID 51 | 83f2618b-c486-4c1b-8a6d-9c285d72cf96 52 | 53 | PayloadVersion 54 | 1 55 | 56 | 57 | CalDAVAccountDescription 58 | etesync 59 | 60 | CalDAVHostName 61 | 127.0.0.1 62 | 63 | CalDAVUsername 64 | etesync 65 | 66 | CalDAVPassword 67 | etesync 68 | 69 | PayloadDescription 70 | Configures CalDAV account 71 | 72 | PayloadIdentifier 73 | com.etesync.ios.caldav 74 | 75 | PayloadType 76 | com.apple.caldav.account 77 | 78 | PayloadUUID 79 | 83f2618b-c486-4c1b-8a6d-9c285d72cf95 80 | 81 | PayloadVersion 82 | 1 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/AboutScreen.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, FlatList } from "react-native"; 6 | import { Text, List, TouchableRipple, useTheme } from "react-native-paper"; 7 | 8 | import { Title } from "./widgets/Typography"; 9 | import Container from "./widgets/Container"; 10 | import Markdown from "./widgets/Markdown"; 11 | 12 | import { expo } from "../app.json"; 13 | import * as C from "./constants"; 14 | 15 | import * as licenses from "../licenses.json"; 16 | 17 | function generateRenderLicenseItem(pkgLicenses: any) { 18 | return function renderLicense(param: { item: string }) { 19 | const pkgName = param.item; 20 | const pkg = pkgLicenses[pkgName]!; 21 | const { publisher, repository, url } = pkg; 22 | const description = (publisher && (publisher.toLowerCase() !== pkgName.toLowerCase())) ? `${pkg.licenses} by ${publisher}` : pkg.licenses; 23 | const link = repository ?? url; 24 | return ( 25 | ()} 30 | onPress={link && (() => { Linking.openURL(link) })} 31 | /> 32 | ); 33 | }; 34 | } 35 | 36 | const markdownContent = ` 37 | This app is made possible with financial support from [NLnet Foundation](https://nlnet.nl/), courtesy of [NGI0 Discovery](https://nlnet.nl/discovery) and the [European Commission](https://ec.europa.eu) [DG CNECT](https://ec.europa.eu/info/departments/communications-networks-content-and-technology_en)'s [Next Generation Internet](https://ngi.eu) programme. 38 | `; 39 | 40 | export default function AboutScreen() { 41 | const theme = useTheme(); 42 | 43 | return ( 44 | ( 47 | 48 | {C.appName} {expo.version} 49 | { Linking.openURL(C.homePage) }}> 50 | {C.homePage} 51 | 52 | 53 | 54 | Open Source Licenses 55 | 56 | )} 57 | data={Object.keys(licenses.dependencies)} 58 | keyExtractor={(item) => item} 59 | renderItem={generateRenderLicenseItem(licenses.dependencies)} 60 | /> 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | minSdkVersion = 21 6 | targetSdkVersion = 28 7 | compileSdkVersion = 28 8 | 9 | dbFlowVersion = '4.2.4' 10 | buildToolsVersion = '28.0.0' 11 | supportLibVersion = '28.0.0' 12 | kotlinVersion = '1.3.50' 13 | repositoryUrl = "file:${System.env.HOME}/.m2/repository/" 14 | } 15 | repositories { 16 | google() 17 | jcenter() 18 | maven { url 'https://dl.bintray.com/android/android-tools/' } 19 | } 20 | dependencies { 21 | classpath 'com.android.tools.build:gradle:3.5.1' 22 | classpath 'com.google.gms:google-services:3.2.1' 23 | classpath 'de.undercouch:gradle-download-task:3.4.3' 24 | 25 | // https://github.com/awslabs/aws-device-farm-gradle-plugin/releases 26 | classpath 'com.amazonaws:aws-devicefarm-gradle-plugin:1.3' 27 | 28 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 29 | } 30 | } 31 | 32 | allprojects { 33 | repositories { 34 | // For non-detach 35 | maven { 36 | url "$rootDir/maven" 37 | } 38 | // For old expoviews to work 39 | maven { 40 | url "$rootDir/versioned-abis/expoview-abi36_0_0/maven" 41 | } 42 | maven { 43 | url "$rootDir/versioned-abis/expoview-abi33_0_0/maven" 44 | } 45 | maven { 46 | url "$rootDir/versioned-abis/expoview-abi34_0_0/maven" 47 | } 48 | maven { 49 | url "$rootDir/versioned-abis/expoview-abi35_0_0/maven" 50 | } 51 | maven { 52 | url "$rootDir/versioned-abis/maven" 53 | } 54 | // For detach 55 | maven { 56 | url "$rootDir/../node_modules/expokit/maven" 57 | } 58 | maven { 59 | // We use a modified build of com.android.support.test:runner:1.0.1. Explanation in maven-test/README 60 | url "$rootDir/maven-test" 61 | } 62 | google() 63 | jcenter() 64 | maven { 65 | // Local Maven repo containing AARs with JSC built for Android 66 | url "$rootDir/../node_modules/jsc-android/dist" 67 | } 68 | flatDir { 69 | dirs 'libs' 70 | // dirs project(':expoview').file('libs') 71 | } 72 | // https://github.com/google/ExoPlayer/issues/5225#issuecomment-445739013 73 | maven { url 'https://google.bintray.com/exoplayer' } 74 | // Using www.jitpack.io instead of plain jitpack.io due to 75 | // https://github.com/jitpack/jitpack.io/issues/4002 76 | maven { url "https://www.jitpack.io" } 77 | 78 | // Want this last so that we never end up with a stale cache 79 | mavenLocal() 80 | } 81 | } 82 | 83 | task clean(type: Delete) { 84 | delete rootProject.buildDir 85 | } 86 | -------------------------------------------------------------------------------- /android/app/src/main/res/layout/notification.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 19 | 20 | 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/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 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | platform :ios, '10.0' 3 | 4 | target 'etesync' do 5 | pod 'ExpoKit', 6 | :git => "http://github.com/expo/expo.git", 7 | :tag => "ios/2.14.2", 8 | :subspecs => [ 9 | "Core" 10 | ], 11 | :inhibit_warnings => true 12 | 13 | # Install unimodules 14 | require_relative '../node_modules/react-native-unimodules/cocoapods.rb' 15 | use_unimodules!( 16 | modules_paths: ['../node_modules'], 17 | exclude: [ 18 | 'expo-bluetooth', 19 | 'expo-in-app-purchases', 20 | 'expo-payments-stripe', 21 | ], 22 | ) 23 | 24 | # Install React Native and its dependencies 25 | require_relative '../node_modules/react-native/scripts/autolink-ios.rb' 26 | use_react_native! 27 | 28 | pod 'react-native-rsa-native', :path => '../node_modules/react-native-rsa-native' 29 | pod 'react-native-sodium', :path => '../node_modules/react-native-sodium' 30 | pod 'react-native-get-random-values', :path => '../node_modules/react-native-get-random-values' 31 | pod 'MessagePack.swift', '~> 3.0' 32 | 33 | post_install do |installer| 34 | installer.pods_project.main_group.tab_width = '2'; 35 | installer.pods_project.main_group.indent_width = '2'; 36 | 37 | installer.target_installation_results.pod_target_installation_results 38 | .each do |pod_name, target_installation_result| 39 | 40 | if pod_name == 'ExpoKit' 41 | target_installation_result.native_target.build_configurations.each do |config| 42 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)'] 43 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'EX_DETACHED=1' 44 | 45 | # Enable Google Maps support 46 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'HAVE_GOOGLE_MAPS=1' 47 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'HAVE_GOOGLE_MAPS_UTILS=1' 48 | 49 | end 50 | end 51 | 52 | 53 | 54 | target_installation_result.native_target.build_configurations.each do |config| 55 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '10.0' 56 | end 57 | 58 | 59 | # Can't specify this in the React podspec because we need to use those podspecs for detached 60 | # projects which don't reference ExponentCPP. 61 | if pod_name.start_with?('React') 62 | target_installation_result.native_target.build_configurations.each do |config| 63 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '10.0' 64 | config.build_settings['HEADER_SEARCH_PATHS'] ||= ['$(inherited)'] 65 | end 66 | end 67 | 68 | # Build React Native with RCT_DEV enabled and RCT_ENABLE_INSPECTOR and 69 | # RCT_ENABLE_PACKAGER_CONNECTION disabled 70 | next unless pod_name.start_with?('React') 71 | target_installation_result.native_target.build_configurations.each do |config| 72 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)'] 73 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'RCT_DEV=1' 74 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'RCT_ENABLE_INSPECTOR=0' 75 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'ENABLE_PACKAGER_CONNECTION=0' 76 | end 77 | 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "test": "jest --watch", 8 | "check-types": "tsc", 9 | "lint": "eslint --ext js,jsx,ts,tsx src", 10 | "eject": "expo eject" 11 | }, 12 | "dependencies": { 13 | "@expo/vector-icons": "^10.0.0", 14 | "@react-native-community/masked-view": "^0.1.6", 15 | "@react-native-community/netinfo": "4.6.0", 16 | "@react-navigation/drawer": "^5.0.5", 17 | "@react-navigation/native": "^5.0.5", 18 | "@react-navigation/stack": "^5.0.5", 19 | "assert": "^1.4.1", 20 | "buffer": "^5.2.1", 21 | "color": "^3.1.2", 22 | "constants": "^0.0.2", 23 | "crypto": "git+https://github.com/etesync/expo-crypto", 24 | "etebase": "^0.30.0", 25 | "etesync": "^0.3.1", 26 | "expo": "^36.0.0", 27 | "expo-background-fetch": "~8.0.0", 28 | "expo-calendar": "~8.0.0", 29 | "expo-contacts": "~8.0.0", 30 | "expo-random": "~8.0.0", 31 | "expo-secure-store": "^8.0.0", 32 | "expo-task-manager": "~8.0.0", 33 | "expokit": "36.0.0", 34 | "ical.js": "^1.3.0", 35 | "immutable": "^4.0.0-rc.12", 36 | "moment": "^2.24.0", 37 | "react": "16.9.0", 38 | "react-native": "https://github.com/expo/react-native/archive/sdk-36.0.1.tar.gz", 39 | "react-native-appearance": "~0.3.1", 40 | "react-native-etebase": "^0.1.5", 41 | "react-native-gesture-handler": "^1.6.0", 42 | "react-native-get-random-values": "1.4.0", 43 | "react-native-keyboard-aware-scroll-view": "^0.9.1", 44 | "react-native-markdown-display": "^6.1.6", 45 | "react-native-paper": "^3.9.0", 46 | "react-native-reanimated": "^1.7.0", 47 | "react-native-rsa-native": "^1.1.3", 48 | "react-native-safe-area-context": "^0.7.3", 49 | "react-native-screens": "^2.0.0-beta.4", 50 | "react-native-sodium": "^0.3.8", 51 | "react-native-unimodules": "^0.7.0", 52 | "react-native-vector-icons": "^6.6.0", 53 | "react-native-webview": "7.4.3", 54 | "react-redux": "^7.1.0", 55 | "redux": "^4.0.1", 56 | "redux-actions": "^2.6.5", 57 | "redux-logger": "^3.0.6", 58 | "redux-persist": "^6.0.0", 59 | "redux-persist-expo-securestore": "^2.0.0", 60 | "redux-thunk": "^2.3.0" 61 | }, 62 | "devDependencies": { 63 | "@types/color": "^3.0.0", 64 | "@types/jest": "^24.0.5", 65 | "@types/node": "^11.9.4", 66 | "@types/node-rsa": "^1.0.0", 67 | "@types/react": "^16.8.23", 68 | "@types/react-native": "^0.57.65", 69 | "@types/react-native-vector-icons": "^6.4.6", 70 | "@types/react-redux": "^7.1.1", 71 | "@types/redux": "^3.6.0", 72 | "@types/redux-actions": "^2.3.2", 73 | "@types/redux-logger": "^3.0.7", 74 | "@types/sjcl": "^1.0.28", 75 | "@types/urijs": "^1.15.38", 76 | "@types/uuid": "^3.4.4", 77 | "@typescript-eslint/eslint-plugin": "^2.6.1", 78 | "@typescript-eslint/parser": "^2.6.1", 79 | "@typescript-eslint/typescript-estree": "^2.6.1", 80 | "babel-preset-expo": "^7.0.0", 81 | "eslint": "^6.6.0", 82 | "eslint-plugin-react": "^7.16.0", 83 | "jest-expo": "^36.0.0", 84 | "license-checker": "^25.0.1", 85 | "react-test-renderer": "^16.8.6", 86 | "typescript": "^3.9.2" 87 | }, 88 | "jest": { 89 | "preset": "jest-expo" 90 | }, 91 | "private": true 92 | } 93 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/etesync/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "AppIcon20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "AppIcon20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "idiom" : "iphone", 17 | "size" : "29x29", 18 | "scale" : "1x" 19 | }, 20 | { 21 | "size" : "29x29", 22 | "idiom" : "iphone", 23 | "filename" : "AppIcon29x29@2x.png", 24 | "scale" : "2x" 25 | }, 26 | { 27 | "size" : "29x29", 28 | "idiom" : "iphone", 29 | "filename" : "AppIcon29x29@3x.png", 30 | "scale" : "3x" 31 | }, 32 | { 33 | "size" : "40x40", 34 | "idiom" : "iphone", 35 | "filename" : "AppIcon40x40@2x.png", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "size" : "40x40", 40 | "idiom" : "iphone", 41 | "filename" : "AppIcon40x40@3x.png", 42 | "scale" : "3x" 43 | }, 44 | { 45 | "idiom" : "iphone", 46 | "size" : "57x57", 47 | "scale" : "1x" 48 | }, 49 | { 50 | "idiom" : "iphone", 51 | "size" : "57x57", 52 | "scale" : "2x" 53 | }, 54 | { 55 | "size" : "60x60", 56 | "idiom" : "iphone", 57 | "filename" : "AppIcon60x60@2x.png", 58 | "scale" : "2x" 59 | }, 60 | { 61 | "size" : "60x60", 62 | "idiom" : "iphone", 63 | "filename" : "AppIcon60x60@3x.png", 64 | "scale" : "3x" 65 | }, 66 | { 67 | "idiom" : "ipad", 68 | "size" : "20x20", 69 | "scale" : "1x" 70 | }, 71 | { 72 | "idiom" : "ipad", 73 | "size" : "20x20", 74 | "scale" : "2x" 75 | }, 76 | { 77 | "idiom" : "ipad", 78 | "size" : "29x29", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "idiom" : "ipad", 83 | "size" : "29x29", 84 | "scale" : "2x" 85 | }, 86 | { 87 | "idiom" : "ipad", 88 | "size" : "40x40", 89 | "scale" : "1x" 90 | }, 91 | { 92 | "idiom" : "ipad", 93 | "size" : "40x40", 94 | "scale" : "2x" 95 | }, 96 | { 97 | "idiom" : "ipad", 98 | "size" : "50x50", 99 | "scale" : "1x" 100 | }, 101 | { 102 | "idiom" : "ipad", 103 | "size" : "50x50", 104 | "scale" : "2x" 105 | }, 106 | { 107 | "idiom" : "ipad", 108 | "size" : "72x72", 109 | "scale" : "1x" 110 | }, 111 | { 112 | "idiom" : "ipad", 113 | "size" : "72x72", 114 | "scale" : "2x" 115 | }, 116 | { 117 | "size" : "76x76", 118 | "idiom" : "ipad", 119 | "filename" : "AppIcon76x76~ipad.png", 120 | "scale" : "1x" 121 | }, 122 | { 123 | "size" : "76x76", 124 | "idiom" : "ipad", 125 | "filename" : "AppIcon76x76@2x~ipad.png", 126 | "scale" : "2x" 127 | }, 128 | { 129 | "size" : "83.5x83.5", 130 | "idiom" : "ipad", 131 | "filename" : "AppIcon83.5x83.5@2x~ipad.png", 132 | "scale" : "2x" 133 | }, 134 | { 135 | "size" : "1024x1024", 136 | "idiom" : "ios-marketing", 137 | "filename" : "AppIcon1024x1024.png", 138 | "scale" : "1x" 139 | } 140 | ], 141 | "info" : { 142 | "version" : 1, 143 | "author" : "xcode" 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/CollectionMemberAddDialog.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: © 2017 EteSync Authors 2 | // SPDX-License-Identifier: AGPL-3.0-only 3 | 4 | import * as React from "react"; 5 | 6 | import * as Etebase from "etebase"; 7 | 8 | import { Paragraph } from "react-native-paper"; 9 | 10 | import { useCredentials } from "./credentials"; 11 | 12 | import TextInput from "./widgets/TextInput"; 13 | import Checkbox from "./widgets/Checkbox"; 14 | import PrettyFingerprint from "./widgets/PrettyFingerprintEb"; 15 | import LoadingIndicator from "./widgets/LoadingIndicator"; 16 | import ConfirmationDialog from "./widgets/ConfirmationDialog"; 17 | 18 | 19 | interface PropsType { 20 | onOk: (username: string, publicKey: Uint8Array, accessLevel: Etebase.CollectionAccessLevel) => void; 21 | onClose: () => void; 22 | } 23 | 24 | export default function CollectionMemberAddDialog(props: PropsType) { 25 | const etebase = useCredentials()!; 26 | const [publicKey, setPublicKey] = React.useState(); 27 | const [readOnly, setReadOnly] = React.useState(false); 28 | const [userChosen, setUserChosen] = React.useState(false); 29 | const [username, setUsername] = React.useState(""); 30 | const [error, setError] = React.useState(); 31 | 32 | async function onAddRequest() { 33 | setUserChosen(true); 34 | const inviteMgr = etebase.getInvitationManager(); 35 | try { 36 | const userProfile = await inviteMgr.fetchUserProfile(username); 37 | setPublicKey(userProfile.pubkey); 38 | } catch (e) { 39 | setError(e); 40 | } 41 | } 42 | 43 | function onOk() { 44 | props.onOk(username, publicKey!, readOnly ? Etebase.CollectionAccessLevel.ReadOnly : Etebase.CollectionAccessLevel.ReadWrite); 45 | } 46 | 47 | const { onClose } = props; 48 | 49 | if (error) { 50 | return ( 51 | <> 52 | 59 | 60 | User ({username}) not found. Have they setup their encryption password from one of the apps? 61 | 62 | 63 | 64 | ); 65 | } 66 | 67 | if (publicKey) { 68 | return ( 69 | <> 70 | 77 | 78 | Verify {username}'s security fingerprint to ensure the encryption is secure. 79 | 80 | 81 | 82 | 83 | ); 84 | } else { 85 | return ( 86 | <> 87 | 94 | {userChosen ? 95 | 96 | : 97 | <> 98 | 108 | { setReadOnly(!readOnly) }} 112 | /> 113 | 114 | } 115 | 116 | 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /ios/etesync/Supporting/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | EteSync 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | com.etesync.ios 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleLocalizations 16 | 17 | en 18 | 19 | CFBundleName 20 | EteSync 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | $(MARKETING_VERSION) 25 | CFBundleURLTypes 26 | 27 | 28 | CFBundleURLSchemes 29 | 30 | exp2904f03229f24a3ca9b3fcd8485120ca 31 | 32 | 33 | 34 | CFBundleURLName 35 | OAuthRedirect 36 | CFBundleURLSchemes 37 | 38 | com.etesync.ios 39 | 40 | 41 | 42 | CFBundleVersion 43 | $(CURRENT_PROJECT_VERSION) 44 | Fabric 45 | 46 | APIKey 47 | 81130e95ea13cd7ed9a4f455e96214902c721c99 48 | Kits 49 | 50 | 51 | KitInfo 52 | 53 | KitName 54 | Crashlytics 55 | 56 | 57 | 58 | FacebookAdvertiserIDCollectionEnabled 59 | 60 | FacebookAutoInitEnabled 61 | 62 | FacebookAutoLogAppEventsEnabled 63 | 64 | GADApplicationIdentifier 65 | ca-app-pub-3940256099942544~1458002511 66 | GADDelayAppMeasurementInit 67 | 68 | ITSAppUsesNonExemptEncryption 69 | 70 | LSRequiresIPhoneOS 71 | 72 | NSAppTransportSecurity 73 | 74 | NSAllowsArbitraryLoads 75 | 76 | 77 | NSCalendarsUsageDescription 78 | Access to your calendars is needed in order to load and save EteSync calendars. 79 | NSCameraUsageDescription 80 | Allow EteSync to use your camera 81 | NSContactsUsageDescription 82 | Access to your contacts is needed in order to load and save EteSync contacts (including notes). 83 | NSLocationWhenInUseUsageDescription 84 | Allow EteSync to use your location 85 | NSMicrophoneUsageDescription 86 | Allow EteSync to access your microphone 87 | NSMotionUsageDescription 88 | Allow EteSync to access your device's accelerometer 89 | NSPhotoLibraryAddUsageDescription 90 | Give EteSync permission to save photos 91 | NSPhotoLibraryUsageDescription 92 | Give EteSync permission to access your photos 93 | NSRemindersUsageDescription 94 | Access to your reminders is needed in order to load and save EteSync reminders. 95 | UIBackgroundModes 96 | 97 | fetch 98 | 99 | UILaunchStoryboardName 100 | LaunchScreen 101 | UIRequiredDeviceCapabilities 102 | 103 | UIRequiresFullScreen 104 | 105 | UIStatusBarStyle 106 | UIStatusBarStyleLightContent 107 | UISupportedInterfaceOrientations 108 | 109 | UIInterfaceOrientationPortrait 110 | UIInterfaceOrientationLandscapeLeft 111 | UIInterfaceOrientationLandscapeRight 112 | UIInterfaceOrientationPortraitUpsideDown 113 | 114 | UIUserInterfaceStyle 115 | Automatic 116 | UIViewControllerBasedStatusBarAppearance 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /ios/etesync.xcodeproj/xcshareddata/xcschemes/etesync.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 79 | 80 | 81 | 82 | 83 | 84 | 90 | 92 | 98 | 99 | 100 | 101 | 103 | 104 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /ios/etesync/Supporting/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/CollectionItemContact.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 | 7 | import { Clipboard, Linking } from "react-native"; 8 | import { Text, List, Divider } from "react-native-paper"; 9 | 10 | import { DecryptedCollection, DecryptedItem } from "./store"; 11 | 12 | import Container from "./widgets/Container"; 13 | 14 | import { ContactType } from "./pim-types"; 15 | 16 | import JournalItemHeader from "./JournalItemHeader"; 17 | 18 | interface PropsType { 19 | collection: DecryptedCollection; 20 | item: DecryptedItem; 21 | } 22 | 23 | export default React.memo(function CollectionItemContact(props: PropsType) { 24 | const entry = props.item; 25 | const contact = ContactType.parse(entry.content); 26 | 27 | const revProp = contact.comp.getFirstProperty("rev"); 28 | const lastModified = (revProp) ? moment(revProp.getFirstValue().toJSDate()).format("LLLL") : undefined; 29 | 30 | const lists = []; 31 | 32 | function getAllType( 33 | propName: string, 34 | leftIcon: string, 35 | valueToHref?: (value: string, type: string) => string, 36 | primaryTransform?: (value: string, type: string) => string, 37 | secondaryTransform?: (value: string, type: string) => string) { 38 | 39 | return contact.comp.getAllProperties(propName).map((prop, idx) => { 40 | const type = prop.toJSON()[1].type; 41 | const values = prop.getValues().map((val) => { 42 | const primaryText = primaryTransform ? primaryTransform(val, type) : val; 43 | 44 | const href = valueToHref?.(val, type); 45 | const onPress = (href && Linking.canOpenURL(href)) ? (() => { Linking.openURL(href) }) : undefined; 46 | 47 | return ( 48 | Clipboard.setString(primaryText)} 53 | left={(props) => } 54 | description={secondaryTransform ? secondaryTransform(val, type) : type} 55 | /> 56 | ); 57 | }); 58 | return values; 59 | }); 60 | } 61 | 62 | lists.push(getAllType( 63 | "tel", 64 | "phone", 65 | (x) => ("tel:" + x) 66 | )); 67 | 68 | lists.push(getAllType( 69 | "email", 70 | "email", 71 | (x) => ("mailto:" + x) 72 | )); 73 | 74 | lists.push(getAllType( 75 | "impp", 76 | "chat", 77 | (x) => x, 78 | (x) => (x.substring(x.indexOf(":") + 1)), 79 | (x) => (x.substring(0, x.indexOf(":"))) 80 | )); 81 | 82 | lists.push(getAllType( 83 | "adr", 84 | "home" 85 | )); 86 | 87 | lists.push(getAllType( 88 | "bday", 89 | "calendar", 90 | undefined, 91 | ((x: any) => (x.toJSDate) ? moment(x.toJSDate()).format("dddd, LL") : x), 92 | () => "Birthday" 93 | )); 94 | 95 | lists.push(getAllType( 96 | "anniversary", 97 | "calendar", 98 | undefined, 99 | ((x: any) => (x.toJSDate) ? moment(x.toJSDate()).format("dddd, LL") : x), 100 | () => "Anniversary" 101 | )); 102 | 103 | const skips = ["tel", "email", "impp", "adr", "bday", "anniversary", "rev", 104 | "prodid", "uid", "fn", "n", "version", "photo"]; 105 | const theRest = contact.comp.getAllProperties().filter((prop) => ( 106 | skips.indexOf(prop.name) === -1 107 | )).map((prop, idx) => { 108 | const values = prop.getValues().map((_val) => { 109 | const val = (_val instanceof String) ? _val : _val.toString(); 110 | return ( 111 | Clipboard.setString(val)} 115 | description={prop.name} 116 | /> 117 | ); 118 | }); 119 | return values; 120 | }); 121 | 122 | function listIfNotEmpty(items: JSX.Element[][]) { 123 | if (items.length > 0) { 124 | return ( 125 | 126 | {items} 127 | 128 | 129 | ); 130 | } else { 131 | return undefined; 132 | } 133 | } 134 | 135 | return ( 136 | <> 137 | 138 | {lastModified && ( 139 | Modified: {lastModified} 140 | )} 141 | 142 | 143 | {lists.map((list, idx) => ( 144 | 145 | {listIfNotEmpty(list)} 146 | 147 | ))} 148 | {theRest} 149 | 150 | 151 | ); 152 | }); 153 | -------------------------------------------------------------------------------- /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/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 | --------------------------------------------------------------------------------