diff --git a/.circleci/changelog.sh b/.circleci/changelog.sh old mode 100644 new mode 100755 diff --git a/.circleci/config.yml b/.circleci/config.yml index 95ae45cbd..94faa75cd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,7 +28,7 @@ jobs: - run: name: Test command: | - npm test + npm run test - run: name: Codecov @@ -151,9 +151,7 @@ jobs: name: Install NPM modules command: | rm -rf node_modules - # npm install --save react-native@0.51 npm install - # npm install react-native - run: name: Fix known build error @@ -227,6 +225,7 @@ workflows: filters: branches: only: + - beta - develop - master # - ios-testflight: diff --git a/README.md b/README.md index 321649b79..e764f1a20 100644 --- a/README.md +++ b/README.md @@ -22,20 +22,20 @@ Follow the [React Native Getting Started Guide](https://facebook.github.io/react $ git clone git@github.com:RocketChat/Rocket.Chat.ReactNative.git $ cd Rocket.Chat.ReactNative $ npm install -g react-native-cli - $ yarn + $ npm install ``` - Configuration ```bash - $ yarn fabric-ios --key="YOUR_API_KEY" --secret="YOUR_API_SECRET" - $ yarn fabric-android --key="YOUR_API_KEY" --secret="YOUR_API_SECRET" + $ npm run fabric-ios --key="YOUR_API_KEY" --secret="YOUR_API_SECRET" + $ npm run fabric-android --key="YOUR_API_KEY" --secret="YOUR_API_SECRET" ``` - Run application ```bash - $ yarn ios + $ npm run ios ``` ```bash - $ yarn android + $ npm run android ``` # Storybook diff --git a/__tests__/__snapshots__/RoomItem.js.snap b/__tests__/__snapshots__/RoomItem.js.snap index 81cea847b..7322a46b6 100644 --- a/__tests__/__snapshots__/RoomItem.js.snap +++ b/__tests__/__snapshots__/RoomItem.js.snap @@ -46,7 +46,7 @@ exports[`render channel 1`] = ` }, Object { "backgroundColor": "#00BCD4", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -147,10 +147,8 @@ exports[`render channel 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> @@ -206,7 +204,7 @@ exports[`render no icon 1`] = ` }, Object { "backgroundColor": "#3F51B5", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -307,10 +305,8 @@ exports[`render no icon 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> @@ -366,7 +362,7 @@ exports[`render private group 1`] = ` }, Object { "backgroundColor": "#FF9800", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -467,10 +463,8 @@ exports[`render private group 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> @@ -527,7 +521,7 @@ exports[`render unread +999 1`] = ` }, Object { "backgroundColor": "#3F51B5", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -561,13 +555,16 @@ exports[`render unread +999 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -639,10 +636,8 @@ exports[`render unread +999 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } > @@ -721,7 +716,7 @@ exports[`render unread 1`] = ` }, Object { "backgroundColor": "#3F51B5", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -755,13 +750,16 @@ exports[`render unread 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -833,10 +831,8 @@ exports[`render unread 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } > @@ -915,7 +911,7 @@ exports[`renders correctly 1`] = ` }, Object { "backgroundColor": "#3F51B5", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -949,13 +945,16 @@ exports[`renders correctly 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -1027,10 +1026,8 @@ exports[`renders correctly 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap index 86e9a4e19..a735dc7f5 100644 --- a/__tests__/__snapshots__/Storyshots.test.js.snap +++ b/__tests__/__snapshots__/Storyshots.test.js.snap @@ -12,7 +12,7 @@ exports[`Storyshots Avatar avatar 1`] = ` }, Object { "backgroundColor": "#3F51B5", - "borderRadius": 4, + "borderRadius": 2, "height": 25, "width": 25, }, @@ -48,7 +48,7 @@ exports[`Storyshots Avatar avatar 1`] = ` }, Object { "backgroundColor": "#9C27B0", - "borderRadius": 4, + "borderRadius": 2, "height": 40, "width": 40, }, @@ -84,7 +84,7 @@ exports[`Storyshots Avatar avatar 1`] = ` }, Object { "backgroundColor": "#9C27B0", - "borderRadius": 4, + "borderRadius": 2, "height": 30, "width": 30, }, @@ -198,7 +198,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#8BC34A", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -232,13 +232,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -310,10 +313,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> @@ -364,7 +365,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#8BC34A", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -398,13 +399,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -480,10 +484,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> @@ -534,7 +536,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#8BC34A", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -568,13 +570,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -646,10 +651,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } > @@ -723,7 +726,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#795548", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -757,13 +760,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -839,10 +845,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } > @@ -916,7 +920,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#795548", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -950,13 +954,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -1028,10 +1035,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } > @@ -1105,7 +1110,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#795548", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -1139,13 +1144,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -1217,10 +1225,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } > @@ -1294,7 +1300,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#795548", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -1328,13 +1334,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -1406,10 +1415,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } > @@ -1483,7 +1490,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#795548", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -1517,13 +1524,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -1595,10 +1605,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } > @@ -1672,7 +1680,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#E91E63", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -1706,13 +1714,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -1784,10 +1795,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> @@ -1838,7 +1847,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": "#9C27B0", - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -1872,13 +1881,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -1950,10 +1962,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> @@ -2004,7 +2014,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` }, Object { "backgroundColor": undefined, - "borderRadius": 4, + "borderRadius": 2, "height": 46, "width": 46, }, @@ -2038,13 +2048,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` "height": 16, "width": 16, }, - Object { - "borderColor": "#fff", - "borderWidth": 3, - "bottom": -3, - "position": "absolute", - "right": -3, - }, + Array [ + Object { + "borderColor": "#fff", + "borderWidth": 3, + "bottom": -3, + "position": "absolute", + "right": -3, + }, + undefined, + ], Object { "backgroundColor": "#cbced1", }, @@ -2116,10 +2129,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = ` style={ Object { "alignItems": "flex-end", - "flex": 1, "flexDirection": "row", "justifyContent": "flex-end", - "width": "100%", } } /> diff --git a/android/app/build.gradle b/android/app/build.gradle index 1183adc62..7efa4b2a2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -82,12 +82,12 @@ apply from: "../../node_modules/react-native/react.gradle" * Upload all the APKs to the Play Store and people will download * the correct one based on the CPU architecture of their device. */ -def enableSeparateBuildPerCPUArchitecture = false +def enableSeparateBuildPerCPUArchitecture = true /** * Run Proguard to shrink the Java bytecode in release builds. */ -def enableProguardInReleaseBuilds = false +def enableProguardInReleaseBuilds = true android { compileSdkVersion 25 @@ -98,11 +98,12 @@ android { minSdkVersion 16 targetSdkVersion 22 versionCode VERSIONCODE as Integer - versionName "1.1" + versionName "1" ndk { abiFilters "armeabi-v7a", "x86" } } + signingConfigs { release { if (project.hasProperty('KEYSTORE')) { @@ -123,10 +124,16 @@ android { } buildTypes { release { - minifyEnabled enableProguardInReleaseBuilds - proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" signingConfig signingConfigs.release + shrinkResources enableProguardInReleaseBuilds + zipAlignEnabled enableProguardInReleaseBuilds + minifyEnabled enableProguardInReleaseBuilds + useProguard enableProguardInReleaseBuilds + setProguardFiles([getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro']) } + debug { + applicationIdSuffix ".debug" + } } // applicationVariants are e.g. debug, release applicationVariants.all { variant -> diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 6e8516c8d..072b6d4c9 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -18,7 +18,7 @@ # Disabling obfuscation is useful if you collect stack traces from production crashes # (unless you are using a system that supports de-obfuscate the stack traces). --dontobfuscate +# -dontobfuscate # React Native @@ -49,6 +49,7 @@ -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; } -dontwarn com.facebook.react.** +-keep,includedescriptorclasses class com.facebook.react.bridge.** { *; } # TextLayoutBuilder uses a non-public Android constructor within StaticLayout. # See libs/proxy/src/main/java/com/facebook/fbui/textlayoutbuilder/proxy for details. @@ -68,3 +69,25 @@ -dontwarn java.nio.file.* -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement -dontwarn okio.** + +# Fresco +# Keep our interfaces so they can be used by other ProGuard rules. +# See http://sourceforge.net/p/proguard/bugs/466/ +-keep,allowobfuscation @interface com.facebook.soloader.DoNotOptimize + +# Do not strip any method/class that is annotated with @DoNotOptimize +-keep @com.facebook.soloader.DoNotOptimize class * +-keepclassmembers class * { + @com.facebook.soloader.DoNotOptimize *; +} + +# Keep native methods +-keepclassmembers class * { + native ; +} + +# For Fabric to properly de-obfuscate your crash reports, you need to remove this line from your ProGuard config: + -printmapping mapping.txt + +-dontwarn javax.annotation.** +-dontwarn com.facebook.infer.** diff --git a/android/app/src/debug/res/mipmap-hdpi/ic_launcher.png b/android/app/src/debug/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 000000000..ea0f58ac0 Binary files /dev/null and b/android/app/src/debug/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/debug/res/mipmap-mdpi/ic_launcher.png b/android/app/src/debug/res/mipmap-mdpi/ic_launcher.png new file mode 100755 index 000000000..f98ca51c4 Binary files /dev/null and b/android/app/src/debug/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/debug/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/debug/res/mipmap-xhdpi/ic_launcher.png new file mode 100755 index 000000000..9706b70d2 Binary files /dev/null and b/android/app/src/debug/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 000000000..1ee4de388 Binary files /dev/null and b/android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 000000000..555c58844 Binary files /dev/null and b/android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/debug/res/values/colors.xml b/android/app/src/debug/res/values/colors.xml new file mode 100644 index 000000000..56779d1e1 --- /dev/null +++ b/android/app/src/debug/res/values/colors.xml @@ -0,0 +1 @@ + #660B0B0B \ No newline at end of file diff --git a/android/app/src/debug/res/values/strings.xml b/android/app/src/debug/res/values/strings.xml new file mode 100644 index 000000000..631d28ea8 --- /dev/null +++ b/android/app/src/debug/res/values/strings.xml @@ -0,0 +1,5 @@ + + [DEVELOP] RocketChatRN + + No Browser Found + diff --git a/android/app/src/debug/res/values/styles.xml b/android/app/src/debug/res/values/styles.xml new file mode 100644 index 000000000..654ec9502 --- /dev/null +++ b/android/app/src/debug/res/values/styles.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0137ae666..c61f8cd8c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -26,7 +26,9 @@ android:allowBackup="true" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme" + android:resizeableActivity="true" + android:largeHeap="true"> ; + +export default RCActivityIndicator; diff --git a/app/containers/Avatar.js b/app/containers/Avatar.js index 3ab6c266b..6842218e2 100644 --- a/app/containers/Avatar.js +++ b/app/containers/Avatar.js @@ -36,7 +36,7 @@ export default class Avatar extends React.PureComponent { }; render() { const { - text = '', size = 25, baseUrl, borderRadius = 4, style, avatar, type = 'd' + text = '', size = 25, baseUrl, borderRadius = 2, style, avatar, type = 'd' } = this.props; const { initials, color } = avatarInitialsAndColor(`${ text }`); diff --git a/app/containers/Banner.js b/app/containers/Banner.js index 5cc765c39..ae7b75d1f 100644 --- a/app/containers/Banner.js +++ b/app/containers/Banner.js @@ -6,11 +6,7 @@ import { connect } from 'react-redux'; const styles = StyleSheet.create({ bannerContainer: { - backgroundColor: '#ddd', - position: 'absolute', - top: '0%', - zIndex: 10, - width: '100%' + backgroundColor: '#ddd' }, bannerText: { textAlign: 'center', @@ -21,7 +17,8 @@ const styles = StyleSheet.create({ @connect(state => ({ connecting: state.meteor.connecting, authenticating: state.login.isFetching, - offline: !state.meteor.connected + offline: !state.meteor.connected, + logged: !!state.login.token })) export default class Banner extends React.PureComponent { @@ -31,7 +28,9 @@ export default class Banner extends React.PureComponent { offline: PropTypes.bool } render() { - const { connecting, authenticating, offline } = this.props; + const { + connecting, authenticating, offline, logged + } = this.props; if (offline) { return ( @@ -40,6 +39,7 @@ export default class Banner extends React.PureComponent { ); } + if (connecting) { return ( @@ -56,6 +56,14 @@ export default class Banner extends React.PureComponent { ); } - return null; + if (logged) { + return this.props.children; + } + + return ( + + Not logged... + + ); } } diff --git a/app/containers/Button/index.js b/app/containers/Button/index.js new file mode 100644 index 000000000..7477060d9 --- /dev/null +++ b/app/containers/Button/index.js @@ -0,0 +1,85 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, View, Text, Platform } from 'react-native'; + +import { COLOR_BUTTON_PRIMARY, COLOR_TEXT } from '../../constants/colors'; +import Touch from '../../utils/touch'; + +const colors = { + backgroundPrimary: COLOR_BUTTON_PRIMARY, + backgroundSecondary: 'white', + + textColorPrimary: 'white', + textColorSecondary: COLOR_TEXT +}; + +const styles = StyleSheet.create({ + container: { + paddingHorizontal: 15, + paddingVertical: 10, + borderRadius: 2 + }, + text: { + textAlign: 'center', + fontWeight: '700' + }, + background_primary: { + backgroundColor: colors.backgroundPrimary + }, + background_secondary: { + backgroundColor: colors.backgroundSecondary + }, + text_color_primary: { + color: colors.textColorPrimary + }, + text_color_secondary: { + color: colors.textColorSecondary + }, + margin: { + marginBottom: 10 + }, + disabled: { + opacity: 0.5 + } +}); + +export default class Button extends React.PureComponent { + static propTypes = { + title: PropTypes.string, + type: PropTypes.string, + onPress: PropTypes.func, + disabled: PropTypes.bool + } + + static defaultProps = { + title: 'Press me!', + type: 'primary', + onPress: () => alert('It works!'), + disabled: false + } + + render() { + const { + title, type, onPress, disabled + } = this.props; + return ( + + + {title} + + + ); + } +} diff --git a/app/containers/CloseModalButton.js b/app/containers/CloseModalButton.js new file mode 100644 index 000000000..5e60c0a12 --- /dev/null +++ b/app/containers/CloseModalButton.js @@ -0,0 +1,36 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { TouchableOpacity, StyleSheet } from 'react-native'; +import Icon from 'react-native-vector-icons/MaterialIcons'; +import { NavigationActions } from 'react-navigation'; +import { COLOR_TEXT } from '../constants/colors'; + +const styles = StyleSheet.create({ + button: { + width: 25, + height: 25, + marginTop: 5 + }, + icon: { + color: COLOR_TEXT, + left: -5 + } +}); + +export default class CloseModalButton extends React.PureComponent { + static propTypes = { + navigation: PropTypes.object.isRequired + } + + render() { + return ( + this.props.navigation.dispatch(NavigationActions.back())} style={styles.button}> + + + ); + } +} diff --git a/app/containers/EmojiPicker/index.js b/app/containers/EmojiPicker/index.js index 104b496a3..e3edbe1bf 100644 --- a/app/containers/EmojiPicker/index.js +++ b/app/containers/EmojiPicker/index.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { ScrollView } from 'react-native'; import ScrollableTabView from 'react-native-scrollable-tab-view'; -import _ from 'lodash'; +import map from 'lodash/map'; import { emojify } from 'react-emojione'; import TabBar from './TabBar'; import EmojiCategory from './EmojiCategory'; @@ -78,7 +78,7 @@ export default class EmojiPicker extends Component { return emojiRow.length ? emojiRow[0].count + 1 : 1; } updateFrequentlyUsed() { - const frequentlyUsed = _.map(this.frequentlyUsed.slice(), (item) => { + const frequentlyUsed = map(this.frequentlyUsed.slice(), (item) => { if (item.isCustom) { return item; } @@ -88,7 +88,7 @@ export default class EmojiPicker extends Component { } updateCustomEmojis() { - const customEmojis = _.map(this.customEmojis.slice(), item => + const customEmojis = map(this.customEmojis.slice(), item => ({ content: item.name, extension: item.extension, isCustom: true })); this.setState({ customEmojis }); } diff --git a/app/containers/Loading.js b/app/containers/Loading.js new file mode 100644 index 000000000..4e4f795c0 --- /dev/null +++ b/app/containers/Loading.js @@ -0,0 +1,103 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, View, Modal, Animated } from 'react-native'; + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.25)' + }, + image: { + width: 100, + height: 100, + resizeMode: 'contain' + } +}); + +export default class Loading extends React.PureComponent { + static propTypes = { + visible: PropTypes.bool.isRequired + } + + state = { + scale: new Animated.Value(1), + opacity: new Animated.Value(0) + } + + componentDidMount() { + this.opacityAnimation = Animated.timing( + this.state.opacity, + { + toValue: 1, + duration: 1000, + useNativeDriver: true + } + ); + this.scaleAnimation = Animated.loop(Animated.sequence([ + Animated.timing( + this.state.scale, + { + toValue: 0, + duration: 1000, + useNativeDriver: true + } + ), + Animated.timing( + this.state.scale, + { + toValue: 1, + duration: 1000, + useNativeDriver: true + } + ) + ])); + + if (this.props.visible) { + this.startAnimations(); + } + } + + componentDidUpdate(prevProps) { + if (this.props.visible && this.props.visible !== prevProps.visible) { + this.startAnimations(); + } + } + + componentWillUnmount() { + this.opacityAnimation.stop(); + this.scaleAnimation.stop(); + } + + startAnimations() { + this.opacityAnimation.start(); + this.scaleAnimation.start(); + } + + render() { + const scale = this.state.scale.interpolate({ + inputRange: [0, 0.5, 1], + outputRange: [1, 1.1, 1] + }); + return ( + {}} + > + + + + + ); + } +} diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js index d88dcaa95..24ec221a6 100644 --- a/app/containers/MessageBox/index.js +++ b/app/containers/MessageBox/index.js @@ -80,6 +80,7 @@ export default class MessageBox extends React.PureComponent { onChangeText(text) { this.setState({ text }); + this.props.typing(text.length > 0); requestAnimationFrame(() => { const { start, end } = this.component._lastNativeSelection; @@ -174,11 +175,11 @@ export default class MessageBox extends React.PureComponent { }; ImagePicker.showImagePicker(options, (response) => { if (response.didCancel) { - console.log('User cancelled image picker'); + console.warn('User cancelled image picker'); } else if (response.error) { - console.log('ImagePicker Error: ', response.error); + console.warn('ImagePicker Error: ', response.error); } else if (response.customButton) { - console.log('User tapped custom button: ', response.customButton); + console.warn('User tapped custom button: ', response.customButton); } else { const fileInfo = { name: response.fileName, @@ -278,7 +279,7 @@ export default class MessageBox extends React.PureComponent { }); }); } catch (e) { - console.log('spotlight canceled'); + console.warn('spotlight canceled'); } finally { delete this.oldPromise; this.users = database.objects('users').filtered('username CONTAINS[c] $0', keyword).slice(); @@ -321,7 +322,7 @@ export default class MessageBox extends React.PureComponent { this.roomsCache = [...this.roomsCache, ...results.rooms].filter(onlyUnique); this.setState({ mentions: [...rooms.slice(), ...results.rooms] }); } catch (e) { - console.log('spotlight canceled'); + console.warn('spotlight canceled'); } finally { delete this.oldPromise; } @@ -454,7 +455,6 @@ export default class MessageBox extends React.PureComponent { style={{ margin: 8 }} text={item.username || item.name} size={30} - baseUrl={this.props.baseUrl} />, { item.username || item.name } ] diff --git a/app/containers/MessageBox/styles.js b/app/containers/MessageBox/styles.js index 5c0388bc1..dd10d720e 100644 --- a/app/containers/MessageBox/styles.js +++ b/app/containers/MessageBox/styles.js @@ -22,8 +22,9 @@ export default StyleSheet.create({ maxHeight: 120, flexGrow: 1, width: 1, - paddingTop: 15, - paddingBottom: 15, + // paddingVertical: 12, needs to be paddingTop/paddingBottom because of iOS/Android's TextInput differences on rendering + paddingTop: 12, + paddingBottom: 12, paddingLeft: 0, paddingRight: 0 }, @@ -35,7 +36,7 @@ export default StyleSheet.create({ fontSize: 20, textAlign: 'center', padding: 15, - paddingHorizontal: 21, + paddingHorizontal: 12, flex: 0 }, mentionList: { diff --git a/app/containers/TextInput.js b/app/containers/TextInput.js index 3abf75459..389f4b63e 100644 --- a/app/containers/TextInput.js +++ b/app/containers/TextInput.js @@ -1,26 +1,31 @@ import React from 'react'; -import { View, StyleSheet, Text, TextInput } from 'react-native'; +import { View, StyleSheet, Text, TextInput, ViewPropTypes, Platform } from 'react-native'; import PropTypes from 'prop-types'; -import Icon from 'react-native-vector-icons/FontAwesome'; +import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import sharedStyles from '../views/Styles'; -import { COLOR_DANGER } from '../constants/colors'; +import { COLOR_DANGER, COLOR_TEXT } from '../constants/colors'; const styles = StyleSheet.create({ inputContainer: { - marginBottom: 20 + marginBottom: 15 }, label: { - marginBottom: 4, - fontSize: 16 + marginBottom: 10, + color: COLOR_TEXT, + fontSize: 14, + fontWeight: '700' }, input: { + fontSize: 14, paddingTop: 12, paddingBottom: 12, + // paddingTop: 5, + // paddingBottom: 5, paddingHorizontal: 10, borderWidth: 2, - borderRadius: 2, + borderRadius: 4, backgroundColor: 'white', borderColor: 'rgba(0,0,0,.15)', color: 'black' @@ -33,14 +38,23 @@ const styles = StyleSheet.create({ borderColor: COLOR_DANGER }, wrap: { - flex: 1, position: 'relative' }, icon: { position: 'absolute', - right: 0, - padding: 10, - color: 'rgba(0,0,0,.45)' + color: 'rgba(0,0,0,.45)', + height: 45, + textAlignVertical: 'center', + ...Platform.select({ + ios: { + padding: 12 + }, + android: { + paddingHorizontal: 12, + paddingTop: 18, + paddingBottom: 6 + } + }) } }); @@ -49,7 +63,10 @@ export default class RCTextInput extends React.PureComponent { static propTypes = { label: PropTypes.string, error: PropTypes.object, - secureTextEntry: PropTypes.bool + secureTextEntry: PropTypes.bool, + containerStyle: ViewPropTypes.style, + inputStyle: PropTypes.object, + inputRef: PropTypes.func } static defaultProps = { error: {} @@ -58,28 +75,40 @@ export default class RCTextInput extends React.PureComponent { showPassword: false } - get icon() { return ; } + icon = ({ name, onPress, style }) => - tooglePassword = () => this.setState({ showPassword: !this.state.showPassword }) + iconLeft = name => this.icon({ name, onPress: null, style: { left: 0 } }); + + iconPassword = name => this.icon({ name, onPress: () => this.tooglePassword(), style: { right: 0 } }); + + tooglePassword = () => this.setState({ showPassword: !this.state.showPassword }); render() { const { - label, error, secureTextEntry, ...inputProps + label, error, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, ...inputProps } = this.props; const { showPassword } = this.state; return ( - + { label && {label} } - {secureTextEntry && this.icon} + {iconLeft && this.iconLeft(iconLeft)} + {secureTextEntry && this.iconPassword(showPassword ? 'eye-off' : 'eye')} {error.error && {error.reason}} diff --git a/app/containers/Typing.js b/app/containers/Typing.js index 0e2db6e1f..eb667093b 100644 --- a/app/containers/Typing.js +++ b/app/containers/Typing.js @@ -1,16 +1,17 @@ import React from 'react'; - import PropTypes from 'prop-types'; -import { StyleSheet, Text, Keyboard } from 'react-native'; +import { View, StyleSheet, Text, Keyboard, LayoutAnimation } from 'react-native'; import { connect } from 'react-redux'; const styles = StyleSheet.create({ typing: { - transform: [{ scaleY: -1 }], fontWeight: 'bold', paddingHorizontal: 15, height: 25 + }, + emptySpace: { + height: 5 } }); @@ -18,11 +19,13 @@ const styles = StyleSheet.create({ username: state.login.user && state.login.user.username, usersTyping: state.room.usersTyping })) - export default class Typing extends React.Component { shouldComponentUpdate(nextProps) { return this.props.usersTyping.join() !== nextProps.usersTyping.join(); } + componentWillUpdate() { + LayoutAnimation.easeInEaseOut(); + } onPress = () => { Keyboard.dismiss(); } @@ -31,7 +34,13 @@ export default class Typing extends React.Component { return users.length ? `${ users.join(' ,') } ${ users.length > 1 ? 'are' : 'is' } typing` : ''; } render() { - return ( this.onPress()}>{this.usersTyping}); + const { usersTyping } = this; + + if (!usersTyping) { + return ; + } + + return ( this.onPress()}>{usersTyping}); } } diff --git a/app/containers/message/Audio.js b/app/containers/message/Audio.js index d9db162a2..c809dee80 100644 --- a/app/containers/message/Audio.js +++ b/app/containers/message/Audio.js @@ -4,9 +4,9 @@ import { View, StyleSheet, TouchableOpacity, Text, Easing } from 'react-native'; import Video from 'react-native-video'; import Icon from 'react-native-vector-icons/MaterialIcons'; import Slider from 'react-native-slider'; +import { connect } from 'react-redux'; import Markdown from './Markdown'; - const styles = StyleSheet.create({ audioContainer: { flex: 1, @@ -61,6 +61,9 @@ const formatTime = (t = 0, duration = 0) => { return `${ formattedMinutes }:${ formattedSeconds }`; }; +@connect(state => ({ + baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' +})) export default class Audio extends React.PureComponent { static propTypes = { file: PropTypes.object.isRequired, @@ -115,8 +118,8 @@ export default class Audio extends React.PureComponent { const { uri, paused } = this.state; const { description } = this.props.file; return ( - - + [ + - - - + , + + ] ); } } diff --git a/app/containers/message/Emoji.js b/app/containers/message/Emoji.js index edbcd470c..5f87f64e0 100644 --- a/app/containers/message/Emoji.js +++ b/app/containers/message/Emoji.js @@ -2,8 +2,12 @@ import React from 'react'; import { Text, ViewPropTypes } from 'react-native'; import PropTypes from 'prop-types'; import { emojify } from 'react-emojione'; +import { connect } from 'react-redux'; import CustomEmoji from '../EmojiPicker/CustomEmoji'; +@connect(state => ({ + customEmojis: state.customEmojis +})) export default class Emoji extends React.PureComponent { static propTypes = { content: PropTypes.string, diff --git a/app/containers/message/Image.js b/app/containers/message/Image.js index 942676dcc..2bf78435c 100644 --- a/app/containers/message/Image.js +++ b/app/containers/message/Image.js @@ -1,41 +1,30 @@ import PropTypes from 'prop-types'; import React from 'react'; import { CachedImage } from 'react-native-img-cache'; -import { Text, TouchableOpacity, View, StyleSheet } from 'react-native'; +import { TouchableOpacity, StyleSheet } from 'react-native'; +import { connect } from 'react-redux'; import PhotoModal from './PhotoModal'; +import Markdown from './Markdown'; const styles = StyleSheet.create({ button: { flex: 1, - flexDirection: 'column', - height: 320, - borderColor: '#ccc', - borderWidth: 1, - borderRadius: 6 + flexDirection: 'column' }, image: { - flex: 1, - height: undefined, - width: undefined, - resizeMode: 'contain' + width: 320, + height: 200, + resizeMode: 'cover' }, labelContainer: { - height: 62, - alignItems: 'center', - justifyContent: 'center' - }, - imageName: { - fontSize: 12, - alignSelf: 'center', - fontStyle: 'italic' - }, - message: { - alignSelf: 'center', - fontWeight: 'bold' + alignItems: 'flex-start' } }); -export default class Image extends React.PureComponent { +@connect(state => ({ + baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' +})) +export default class extends React.PureComponent { static propTypes = { file: PropTypes.object.isRequired, baseUrl: PropTypes.string.isRequired, @@ -45,8 +34,9 @@ export default class Image extends React.PureComponent { state = { modalVisible: false }; getDescription() { - if (this.props.file.description) { - return {this.props.file.description}; + const { file, customEmojis } = this.props; + if (file.description) { + return ; } } @@ -60,8 +50,9 @@ export default class Image extends React.PureComponent { const { baseUrl, file, user } = this.props; const img = `${ baseUrl }${ file.image_url }?rc_uid=${ user.id }&rc_token=${ user.token }`; return ( - + [ this._onPressButton()} style={styles.button} > @@ -69,18 +60,16 @@ export default class Image extends React.PureComponent { style={styles.image} source={{ uri: encodeURI(img) }} /> - - {this.props.file.title} - {this.getDescription()} - - + {this.getDescription()} + , this.setState({ modalVisible: false })} /> - + ] ); } } diff --git a/app/containers/message/Markdown.js b/app/containers/message/Markdown.js index 1b50f901d..3c88b09d5 100644 --- a/app/containers/message/Markdown.js +++ b/app/containers/message/Markdown.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import EasyMarkdown from 'react-native-easy-markdown'; // eslint-disable-line import SimpleMarkdown from 'simple-markdown'; import { emojify } from 'react-emojione'; +import { connect } from 'react-redux'; import styles from './styles'; import CustomEmoji from '../EmojiPicker/CustomEmoji'; @@ -17,126 +18,139 @@ const BlockCode = ({ node, state }) => ( ); const mentionStyle = { color: '#13679a' }; -const Markdown = ({ - msg, customEmojis, style, markdownStyle, customRules, renderInline -}) => { - if (!msg) { - return null; - } - msg = emojify(msg, { output: 'unicode' }); - - const defaultRules = { - username: { - order: -1, - match: SimpleMarkdown.inlineRegex(/^@[0-9a-zA-Z-_.]+/), - parse: capture => ({ content: capture[0] }), - react: (node, output, state) => ({ - type: 'custom', - key: state.key, - props: { - children: ( - alert('Username')} - > - {node.content} - - ) - } - }) - }, - heading: { - order: -2, - match: SimpleMarkdown.inlineRegex(/^#[0-9a-zA-Z-_.]+/), - parse: capture => ({ content: capture[0] }), - react: (node, output, state) => ({ - type: 'custom', - key: state.key, - props: { - children: ( - alert('Room')} - > - {node.content} - - ) - } - }) - }, - fence: { - order: -3, - match: SimpleMarkdown.blockRegex(/^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n *)+\n/), - parse: capture => ({ - lang: capture[2] || undefined, - content: capture[3] - }), - react: (node, output, state) => ({ - type: 'custom', - key: state.key, - props: { - children: ( - - ) - } - }) - }, - blockCode: { - order: -4, - match: SimpleMarkdown.blockRegex(/^(```)\s*([\s\S]*?[^`])\s*\1(?!```)/), - parse: capture => ({ content: capture[2] }), - react: (node, output, state) => ({ - type: 'custom', - key: state.key, - props: { - children: ( - - ) - } - }) - }, - customEmoji: { - order: -5, - match: SimpleMarkdown.inlineRegex(/^:([0-9a-zA-Z-_.]+):/), - parse: capture => ({ content: capture }), - react: (node, output, state) => { - const element = { - type: 'custom', - key: state.key, - props: { - children: {node.content[0]} - } - }; - const content = node.content[1]; - const emojiExtension = customEmojis[content]; - if (emojiExtension) { - const emoji = { extension: emojiExtension, content }; - element.props.children = ( - - ); - } - return element; +const defaultRules = { + username: { + order: -1, + match: SimpleMarkdown.inlineRegex(/^@[0-9a-zA-Z-_.]+/), + parse: capture => ({ content: capture[0] }), + react: (node, output, state) => ({ + type: 'custom', + key: state.key, + props: { + children: ( + alert('Username')} + > + {node.content} + + ) } - } - }; - - const codeStyle = StyleSheet.flatten(styles.codeStyle); - style = StyleSheet.flatten(style); - return ( - {msg} - - ); + }) + }, + heading: { + order: -2, + match: SimpleMarkdown.inlineRegex(/^#[0-9a-zA-Z-_.]+/), + parse: capture => ({ content: capture[0] }), + react: (node, output, state) => ({ + type: 'custom', + key: state.key, + props: { + children: ( + alert('Room')} + > + {node.content} + + ) + } + }) + }, + fence: { + order: -3, + match: SimpleMarkdown.blockRegex(/^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n *)+\n/), + parse: capture => ({ + lang: capture[2] || undefined, + content: capture[3] + }), + react: (node, output, state) => ({ + type: 'custom', + key: state.key, + props: { + children: ( + + ) + } + }) + }, + blockCode: { + order: -4, + match: SimpleMarkdown.blockRegex(/^(```)\s*([\s\S]*?[^`])\s*\1(?!```)/), + parse: capture => ({ content: capture[2] }), + react: (node, output, state) => ({ + type: 'custom', + key: state.key, + props: { + children: ( + + ) + } + }) + } }; +const codeStyle = StyleSheet.flatten(styles.codeStyle); + +@connect(state => ({ + customEmojis: state.customEmojis +})) +export default class Markdown extends React.Component { + shouldComponentUpdate(nextProps) { + return nextProps.msg !== this.props.msg; + } + render() { + const { + msg, customEmojis = {}, style, markdownStyle, customRules, renderInline + } = this.props; + if (!msg) { + return null; + } + const m = emojify(msg, { output: 'unicode' }); + + const s = StyleSheet.flatten(style); + return ( + ({ content: capture }), + react: (node, output, state) => { + const element = { + type: 'custom', + key: state.key, + props: { + children: {node.content[0]} + } + }; + const content = node.content[1]; + const emojiExtension = customEmojis[content]; + if (emojiExtension) { + const emoji = { extension: emojiExtension, content }; + element.props.children = ( + + ); + } + return element; + } + }, + ...defaultRules, + ...customRules + }} + renderInline={renderInline} + >{m} + + ); + } +} + Markdown.propTypes = { - msg: PropTypes.string.isRequired, + msg: PropTypes.string, customEmojis: PropTypes.object, // eslint-disable-next-line react/no-typos style: ViewPropTypes.style, @@ -149,5 +163,3 @@ BlockCode.propTypes = { node: PropTypes.object, state: PropTypes.object }; - -export default Markdown; diff --git a/app/containers/message/ReactionsModal.js b/app/containers/message/ReactionsModal.js index 08491518f..8f3f5620f 100644 --- a/app/containers/message/ReactionsModal.js +++ b/app/containers/message/ReactionsModal.js @@ -3,6 +3,7 @@ import { View, Text, TouchableWithoutFeedback, FlatList, StyleSheet } from 'reac import PropTypes from 'prop-types'; import Modal from 'react-native-modal'; import Icon from 'react-native-vector-icons/MaterialIcons'; +import { connect } from 'react-redux'; import Emoji from './Emoji'; const styles = StyleSheet.create({ @@ -52,6 +53,10 @@ const styles = StyleSheet.create({ }); const standardEmojiStyle = { fontSize: 20 }; const customEmojiStyle = { width: 20, height: 20 }; + +@connect(state => ({ + customEmojis: state.customEmojis +})) export default class ReactionsModal extends React.PureComponent { static propTypes = { isVisible: PropTypes.bool.isRequired, diff --git a/app/containers/message/User.js b/app/containers/message/User.js index 4921b48e8..8480f9b7d 100644 --- a/app/containers/message/User.js +++ b/app/containers/message/User.js @@ -7,7 +7,9 @@ import Avatar from '../Avatar'; const styles = StyleSheet.create({ username: { - fontWeight: 'bold' + color: '#000', + fontWeight: '400', + fontSize: 14 }, usernameView: { flexDirection: 'row', @@ -22,7 +24,8 @@ const styles = StyleSheet.create({ time: { fontSize: 10, color: '#888', - paddingLeft: 5 + paddingLeft: 5, + fontWeight: '400' }, edited: { marginLeft: 5, @@ -35,11 +38,10 @@ export default class User extends React.PureComponent { static propTypes = { item: PropTypes.object.isRequired, Message_TimeFormat: PropTypes.string.isRequired, - onPress: PropTypes.func, - baseUrl: PropTypes.string + onPress: PropTypes.func } - renderEdited(item) { + renderEdited = (item) => { if (!item.editedBy) { return null; } @@ -50,7 +52,6 @@ export default class User extends React.PureComponent { style={{ marginLeft: 5 }} text={item.editedBy.username} size={20} - baseUrl={this.props.baseUrl} avatar={item.avatar} /> diff --git a/app/containers/message/Video.js b/app/containers/message/Video.js index 0b8a019ab..237f5e38b 100644 --- a/app/containers/message/Video.js +++ b/app/containers/message/Video.js @@ -1,8 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, StyleSheet, TouchableOpacity, Image, Platform } from 'react-native'; +import { StyleSheet, TouchableOpacity, Image, Platform } from 'react-native'; import Modal from 'react-native-modal'; import VideoPlayer from 'react-native-video-controls'; +import { connect } from 'react-redux'; import Markdown from './Markdown'; import openLink from '../../utils/openLink'; @@ -27,6 +28,9 @@ const styles = StyleSheet.create({ } }); +@connect(state => ({ + baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' +})) export default class Video extends React.PureComponent { static propTypes = { file: PropTypes.object.isRequired, @@ -55,18 +59,20 @@ export default class Video extends React.PureComponent { const { baseUrl, user } = this.props; const uri = `${ baseUrl }${ video_url }?rc_uid=${ user.id }&rc_token=${ user.token }`; return ( - + [ this.open()} > - + , - + ] ); } } diff --git a/app/containers/message/index.js b/app/containers/message/index.js index babf15cec..399c67214 100644 --- a/app/containers/message/index.js +++ b/app/containers/message/index.js @@ -1,13 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, TouchableHighlight, Text, TouchableOpacity, Vibration, ViewPropTypes } from 'react-native'; +import { View, Text, TouchableOpacity, Vibration, ViewPropTypes } from 'react-native'; import { connect } from 'react-redux'; import Icon from 'react-native-vector-icons/MaterialIcons'; import moment from 'moment'; import equal from 'deep-equal'; import { KeyboardUtils } from 'react-native-keyboard-input'; -import { actionsShow, errorActionsShow, toggleReactionPicker } from '../../actions/messages'; import Image from './Image'; import User from './User'; import Avatar from '../Avatar'; @@ -18,13 +17,54 @@ import Url from './Url'; import Reply from './Reply'; import ReactionsModal from './ReactionsModal'; import Emoji from './Emoji'; -import messageStatus from '../../constants/messagesStatus'; import styles from './styles'; +import { actionsShow, errorActionsShow, toggleReactionPicker } from '../../actions/messages'; +import messagesStatus from '../../constants/messagesStatus'; +import Touch from '../../utils/touch'; + +const getInfoMessage = ({ + t, role, msg, u +}) => { + if (t === 'rm') { + return 'Message removed'; + } else if (t === 'uj') { + return 'Has joined the channel.'; + } else if (t === 'r') { + return `Room name changed to: ${ msg } by ${ u.username }`; + } else if (t === 'message_pinned') { + return 'Message pinned'; + } else if (t === 'ul') { + return 'Has left the channel.'; + } else if (t === 'ru') { + return `User ${ msg } removed by ${ u.username }`; + } else if (t === 'au') { + return `User ${ msg } added by ${ u.username }`; + } else if (t === 'user-muted') { + return `User ${ msg } muted by ${ u.username }`; + } else if (t === 'user-unmuted') { + return `User ${ msg } unmuted by ${ u.username }`; + } else if (t === 'subscription-role-added') { + return `${ msg } was set ${ role } by ${ u.username }`; + } else if (t === 'subscription-role-removed') { + return `${ msg } is no longer ${ role } by ${ u.username }`; + } else if (t === 'room_changed_description') { + return `Room description changed to: ${ msg } by ${ u.username }`; + } else if (t === 'room_changed_announcement') { + return `Room announcement changed to: ${ msg } by ${ u.username }`; + } else if (t === 'room_changed_topic') { + return `Room topic changed to: ${ msg } by ${ u.username }`; + } else if (t === 'room_changed_privacy') { + return `Room type changed to: ${ msg } by ${ u.username }`; + } + return ''; +}; @connect(state => ({ message: state.messages.message, editing: state.messages.editing, - customEmojis: state.customEmojis + customEmojis: state.customEmojis, + Message_TimeFormat: state.settings.Message_TimeFormat, + Message_GroupingPeriod: state.settings.Message_GroupingPeriod }), dispatch => ({ actionsShow: actionMessage => dispatch(actionsShow(actionMessage)), errorActionsShow: actionMessage => dispatch(errorActionsShow(actionMessage)), @@ -35,13 +75,13 @@ export default class Message extends React.Component { status: PropTypes.any, item: PropTypes.object.isRequired, reactions: PropTypes.any.isRequired, - baseUrl: PropTypes.string.isRequired, Message_TimeFormat: PropTypes.string.isRequired, + Message_GroupingPeriod: PropTypes.number.isRequired, + customTimeFormat: PropTypes.string, message: PropTypes.object.isRequired, user: PropTypes.object.isRequired, editing: PropTypes.bool, errorActionsShow: PropTypes.func, - customEmojis: PropTypes.object, toggleReactionPicker: PropTypes.func, onReactionPress: PropTypes.func, style: ViewPropTypes.style, @@ -63,28 +103,35 @@ export default class Message extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - if (!equal(this.props.reactions, nextProps.reactions)) { - return true; - } if (this.state.reactionsModal !== nextState.reactionsModal) { return true; } - return this.props._updatedAt.toGMTString() !== nextProps._updatedAt.toGMTString() || this.props.status !== nextProps.status; + if (this.props.status !== nextProps.status) { + return true; + } + // eslint-disable-next-line + if (!!this.props._updatedAt ^ !!nextProps._updatedAt) { + return true; + } + if (!equal(this.props.reactions, nextProps.reactions)) { + return true; + } + return this.props._updatedAt.toGMTString() !== nextProps._updatedAt.toGMTString(); } onPress = () => { KeyboardUtils.dismiss(); } - onLongPress() { + onLongPress = () => { this.props.onLongPress(this.parseMessage()); } - onErrorPress() { + onErrorPress = () => { this.props.errorActionsShow(this.parseMessage()); } - onReactionPress(emoji) { + onReactionPress = (emoji) => { this.props.onReactionPress(emoji, this.props.item._id); } onClose() { @@ -95,45 +142,9 @@ export default class Message extends React.Component { Vibration.vibrate(50); } - getInfoMessage() { - let message = ''; - const { - t, role, msg, u - } = this.props.item; - - if (t === 'rm') { - message = 'Message removed'; - } else if (t === 'uj') { - message = 'Has joined the channel.'; - } else if (t === 'r') { - message = `Room name changed to: ${ msg } by ${ u.username }`; - } else if (t === 'message_pinned') { - message = 'Message pinned'; - } else if (t === 'ul') { - message = 'Has left the channel.'; - } else if (t === 'ru') { - message = `User ${ msg } removed by ${ u.username }`; - } else if (t === 'au') { - message = `User ${ msg } added by ${ u.username }`; - } else if (t === 'user-muted') { - message = `User ${ msg } muted by ${ u.username }`; - } else if (t === 'user-unmuted') { - message = `User ${ msg } unmuted by ${ u.username }`; - } else if (t === 'subscription-role-added') { - message = `${ msg } was set ${ role } by ${ u.username }`; - } else if (t === 'subscription-role-removed') { - message = `${ msg } is no longer ${ role } by ${ u.username }`; - } else if (t === 'room_changed_description') { - message = `Room description changed to: ${ msg } by ${ u.username }`; - } else if (t === 'room_changed_announcement') { - message = `Room announcement changed to: ${ msg } by ${ u.username }`; - } else if (t === 'room_changed_topic') { - message = `Room topic changed to: ${ msg } by ${ u.username }`; - } else if (t === 'room_changed_privacy') { - message = `Room type changed to: ${ msg } by ${ u.username }`; - } - - return message; + get timeFormat() { + const { customTimeFormat, Message_TimeFormat } = this.props; + return customTimeFormat || Message_TimeFormat; } parseMessage = () => JSON.parse(JSON.stringify(this.props.item)); @@ -163,64 +174,97 @@ export default class Message extends React.Component { } isTemp() { - return this.props.item.status === messageStatus.TEMP || this.props.item.status === messageStatus.ERROR; + return this.props.item.status === messagesStatus.TEMP || this.props.item.status === messagesStatus.ERROR; } hasError() { - return this.props.item.status === messageStatus.ERROR; + return this.props.item.status === messagesStatus.ERROR; } - attachments() { + renderHeader = (username) => { + const { item, previousItem } = this.props; + + if (previousItem && ( + (previousItem.ts.toDateString() === item.ts.toDateString()) && + (previousItem.u.username === item.u.username) && + !(previousItem.groupable === false || item.groupable === false) && + (previousItem.status === item.status) && + (item.ts - previousItem.ts < this.props.Message_GroupingPeriod * 1000) + )) { + return null; + } + + return ( + + + + + ); + } + + renderContent() { + if (this.isInfoMessage()) { + return {getInfoMessage(this.props.item)}; + } + const { item } = this.props; + return ; + } + + renderAttachment() { if (this.props.item.attachments.length === 0) { return null; } const file = this.props.item.attachments[0]; - const { baseUrl, user } = this.props; + const { user } = this.props; if (file.image_type) { - return ; - } else if (file.audio_type) { - return @@ -246,7 +289,7 @@ export default class Message extends React.Component { } return ( - {this.props.item.reactions.map(reaction => this.renderReaction(reaction))} + {this.props.item.reactions.map(this.renderReaction)} this.props.toggleReactionPicker(this.parseMessage())} key='add-reaction' @@ -260,57 +303,42 @@ export default class Message extends React.Component { render() { const { - item, message, editing, baseUrl, customEmojis, style, archived + item, message, editing, style, archived } = this.props; const username = item.alias || item.u.username; const isEditing = message._id === item._id && editing; - const accessibilityLabel = `Message from ${ username } at ${ moment(item.ts).format(this.props.Message_TimeFormat) }, ${ this.props.item.msg }`; + const accessibilityLabel = `Message from ${ username } at ${ moment(item.ts).format(this.timeFormat) }, ${ this.props.item.msg }`; return ( - this.onPress()} - onLongPress={() => this.onLongPress()} - disabled={this.isDeleted() || this.hasError() || archived} + - - {this.renderError()} - - - - - {this.renderMessageContent()} - {this.attachments()} + + {this.renderHeader(username)} + + {this.renderError()} + + {this.renderContent()} + {this.renderAttachment()} {this.renderUrl()} {this.renderReactions()} - {this.state.reactionsModal ? + {this.state.reactionsModal && - : null } - + ); } } diff --git a/app/containers/message/styles.js b/app/containers/message/styles.js index 23911cc48..45a7a8f45 100644 --- a/app/containers/message/styles.js +++ b/app/containers/message/styles.js @@ -1,20 +1,20 @@ import { StyleSheet, Platform } from 'react-native'; export default StyleSheet.create({ - content: { - flexGrow: 1, - flexShrink: 1 + messageContent: { + flex: 1, + marginLeft: 30 }, flex: { flexDirection: 'row', flex: 1 }, message: { - padding: 12, - paddingTop: 6, - paddingBottom: 6, - flexDirection: 'row', - transform: [{ scaleY: -1 }] + paddingHorizontal: 12, + paddingVertical: 3, + flexDirection: 'column', + transform: [{ scaleY: -1 }], + flex: 1 }, textInfo: { fontStyle: 'italic', @@ -27,6 +27,7 @@ export default StyleSheet.create({ width: 16, height: 16 }, + temp: { opacity: 0.3 }, codeStyle: { ...Platform.select({ ios: { fontFamily: 'Courier New' }, @@ -40,7 +41,8 @@ export default StyleSheet.create({ }, reactionsContainer: { flexDirection: 'row', - flexWrap: 'wrap' + flexWrap: 'wrap', + marginTop: 6 }, reactionContainer: { flexDirection: 'row', @@ -70,5 +72,17 @@ export default StyleSheet.create({ }, avatar: { marginRight: 10 + }, + reactedContainer: { + borderColor: '#bde1fe', + backgroundColor: '#f3f9ff' + }, + reactedCountText: { + color: '#4fb0fc' + }, + errorIcon: { + padding: 10, + paddingRight: 12, + paddingLeft: 0 } }); diff --git a/app/containers/routes/AuthRoutes.js b/app/containers/routes/AuthRoutes.js index e0d8c0d2a..ee753d424 100644 --- a/app/containers/routes/AuthRoutes.js +++ b/app/containers/routes/AuthRoutes.js @@ -6,12 +6,13 @@ import RoomsListView from '../../views/RoomsListView'; import RoomView from '../../views/RoomView'; import RoomActionsView from '../../views/RoomActionsView'; import CreateChannelView from '../../views/CreateChannelView'; -import SelectUsersView from '../../views/SelectUsersView'; +import SelectedUsersView from '../../views/SelectedUsersView'; import NewServerView from '../../views/NewServerView'; import StarredMessagesView from '../../views/StarredMessagesView'; import PinnedMessagesView from '../../views/PinnedMessagesView'; import MentionedMessagesView from '../../views/MentionedMessagesView'; import SnippetedMessagesView from '../../views/SnippetedMessagesView'; +import SearchMessagesView from '../../views/SearchMessagesView'; import RoomFilesView from '../../views/RoomFilesView'; import RoomMembersView from '../../views/RoomMembersView'; import RoomInfoView from '../../views/RoomInfoView'; @@ -28,19 +29,22 @@ const AuthRoutes = StackNavigator( CreateChannel: { screen: CreateChannelView, navigationOptions: { - title: 'Create Channel' + title: 'Create Channel', + headerTintColor: '#292E35' } }, - SelectUsers: { - screen: SelectUsersView, + SelectedUsers: { + screen: SelectedUsersView, navigationOptions: { - title: 'Select Users' + title: 'Select Users', + headerTintColor: '#292E35' } }, AddServer: { screen: NewServerView, navigationOptions: { - title: 'New server' + title: 'New server', + headerTintColor: '#292E35' } }, RoomActions: { @@ -78,6 +82,13 @@ const AuthRoutes = StackNavigator( headerTintColor: '#292E35' } }, + SearchMessages: { + screen: SearchMessagesView, + navigationOptions: { + title: 'Search Messages', + headerTintColor: '#292E35' + } + }, RoomFiles: { screen: RoomFilesView, navigationOptions: { diff --git a/app/containers/routes/NavigationService.js b/app/containers/routes/NavigationService.js index 45de3a2c4..11de166e9 100644 --- a/app/containers/routes/NavigationService.js +++ b/app/containers/routes/NavigationService.js @@ -48,6 +48,11 @@ export function goRoom({ rid, name }, counter = 0) { NavigationActions.navigate({ key: `Room-${ rid }`, routeName: 'Room', params: { room: { rid, name }, rid, name } }) ] }); - config.navigator.dispatch(action); } + +export function dispatch(action) { + if (config.navigator) { + config.navigator.dispatch(action); + } +} diff --git a/app/containers/routes/PublicRoutes.js b/app/containers/routes/PublicRoutes.js index 8fec4d74e..2fdc690e4 100644 --- a/app/containers/routes/PublicRoutes.js +++ b/app/containers/routes/PublicRoutes.js @@ -5,74 +5,114 @@ import Icon from 'react-native-vector-icons/FontAwesome'; import ListServerView from '../../views/ListServerView'; import NewServerView from '../../views/NewServerView'; +import LoginSignupView from '../../views/LoginSignupView'; import LoginView from '../../views/LoginView'; import RegisterView from '../../views/RegisterView'; import TermsServiceView from '../../views/TermsServiceView'; import PrivacyPolicyView from '../../views/PrivacyPolicyView'; import ForgotPasswordView from '../../views/ForgotPasswordView'; +import database from '../../lib/realm'; + +const hasServers = () => { + const db = database.databases.serversDB.objects('servers'); + return db.length > 0; +}; + +const ServerStack = StackNavigator({ + ListServer: { + screen: ListServerView, + navigationOptions({ navigation }) { + return { + title: 'Servers', + headerRight: ( + navigation.navigate({ key: 'AddServer', routeName: 'AddServer' })} + style={{ width: 50, alignItems: 'center' }} + accessibilityLabel='Add server' + accessibilityTraits='button' + > + + + ) + }; + } + }, + AddServer: { + screen: NewServerView, + navigationOptions: { + header: null + } + }, + LoginSignup: { + screen: LoginSignupView, + navigationOptions: { + header: null + } + } +}, { + headerMode: 'screen', + initialRouteName: hasServers() ? 'ListServer' : 'AddServer' +}); + +const LoginStack = StackNavigator({ + Login: { + screen: LoginView, + navigationOptions: { + header: null + } + }, + ForgotPassword: { + screen: ForgotPasswordView, + navigationOptions: { + title: 'Forgot my password', + headerTintColor: '#292E35' + } + } +}, { + headerMode: 'screen' +}); + +const RegisterStack = StackNavigator({ + Register: { + screen: RegisterView, + navigationOptions: { + header: null + } + }, + TermsService: { + screen: TermsServiceView, + navigationOptions: { + title: 'Terms of service', + headerTintColor: '#292E35' + } + }, + PrivacyPolicy: { + screen: PrivacyPolicyView, + navigationOptions: { + title: 'Privacy policy', + headerTintColor: '#292E35' + } + } +}, { + headerMode: 'screen' +}); const PublicRoutes = StackNavigator( { - ListServer: { - screen: ListServerView, - navigationOptions({ navigation }) { - return { - title: 'Servers', - headerRight: ( - navigation.navigate({ key: 'AddServer', routeName: 'AddServer' })} - style={{ width: 50, alignItems: 'center' }} - accessibilityLabel='Add server' - accessibilityTraits='button' - > - - - ) - }; - } - }, - AddServer: { - screen: NewServerView, - navigationOptions: { - title: 'New server' - } + Server: { + screen: ServerStack }, Login: { - screen: LoginView, - navigationOptions: { - title: 'Login' - } + screen: LoginStack }, Register: { - screen: RegisterView, - navigationOptions: { - title: 'Register' - } - }, - TermsService: { - screen: TermsServiceView, - navigationOptions: { - title: 'Terms of service' - } - }, - PrivacyPolicy: { - screen: PrivacyPolicyView, - navigationOptions: { - title: 'Privacy policy' - } - }, - ForgotPassword: { - screen: ForgotPasswordView, - navigationOptions: { - title: 'Forgot my password' - } + screen: RegisterStack } }, { - navigationOptions: { - headerTitleAllowFontScaling: false - } + mode: 'modal', + headerMode: 'none' } ); diff --git a/app/lib/createStore.js b/app/lib/createStore.js index 288b64a31..c9188f082 100644 --- a/app/lib/createStore.js +++ b/app/lib/createStore.js @@ -1,8 +1,8 @@ import { createStore as reduxCreateStore, applyMiddleware, compose } from 'redux'; +import Reactotron from 'reactotron-react-native' ; // eslint-disable-line import createSagaMiddleware from 'redux-saga'; -import logger from 'redux-logger'; import applyAppStateListener from 'redux-enhancer-react-native-appstate'; -import Reactotron from 'reactotron-react-native'; // eslint-disable-line + import reducers from '../reducers'; import sagas from '../sagas'; @@ -20,8 +20,7 @@ if (__DEV__) { enhancers = compose( applyAppStateListener(), applyMiddleware(reduxImmutableStateInvariant), - applyMiddleware(sagaMiddleware), - applyMiddleware(logger) + applyMiddleware(sagaMiddleware) ); } else { sagaMiddleware = createSagaMiddleware(); diff --git a/app/lib/ddp.js b/app/lib/ddp.js index 5d6ef612c..a6fd91c34 100644 --- a/app/lib/ddp.js +++ b/app/lib/ddp.js @@ -1,4 +1,23 @@ import EJSON from 'ejson'; +import { Answers } from 'react-native-fabric'; +import { AppState } from 'react-native'; +import debounce from '../utils/debounce'; +// import { AppState, NativeModules } from 'react-native'; +// const { WebSocketModule, BlobManager } = NativeModules; + +// class WS extends WebSocket { +// _close(code?: number, reason?: string): void { +// if (Platform.OS === 'android') { +// WebSocketModule.close(code, reason, this._socketId); +// } else { +// WebSocketModule.close(this._socketId); +// } +// +// if (BlobManager.isAvailable && this._binaryType === 'blob') { +// BlobManager.removeWebSocketHandler(this._socketId); +// } +// } +// } class EventEmitter { constructor() { @@ -9,6 +28,7 @@ class EventEmitter { this.events[event] = []; } this.events[event].push(listener); + return listener; } removeListener(event, listener) { if (typeof this.events[event] === 'object') { @@ -24,7 +44,8 @@ class EventEmitter { try { listener.apply(this, args); } catch (e) { - console.log(e); + Answers.logCustom(e); + console.warn(e); } }); } @@ -34,72 +55,195 @@ class EventEmitter { this.removeListener(event, g); listener.apply(this, args); }); + return listener; } } + export default class Socket extends EventEmitter { - constructor(url) { + constructor(url, login) { super(); - this.url = url.replace(/^http/, 'ws'); + this.state = 'active'; + this.lastping = new Date(); + this._login = login; + this.url = url;// .replace(/^http/, 'ws'); this.id = 0; this.subscriptions = {}; - this._connect(); this.ddp = new EventEmitter(); - this.on('ping', () => this.send({ msg: 'pong' })); + this._logged = false; + const waitTimeout = () => setTimeout(async() => { + // this.connection.ping(); + this.send({ msg: 'ping' }); + this.timeout = setTimeout(() => this.reconnect(), 1000); + }, 40000); + const handlePing = () => { + this.lastping = new Date(); + this.send({ msg: 'pong' }, true); + if (this.timeout) { + clearTimeout(this.timeout); + } + this.timeout = waitTimeout(); + }; + const handlePong = () => { + this.lastping = new Date(); + if (this.timeout) { + clearTimeout(this.timeout); + } + this.timeout = waitTimeout(); + }; + + + AppState.addEventListener('change', (nextAppState) => { + if (this.state && this.state.match(/inactive/) && nextAppState === 'active') { + try { + this.send({ msg: 'ping' }, true); + // this.connection.ping(); + } catch (e) { + this.reconnect(); + } + } + if (this.state && this.state.match(/background/) && nextAppState === 'active') { + this.emit('background'); + } + this.state = nextAppState; + }); + + this.on('pong', handlePong); + this.on('ping', handlePing); + this.on('result', data => this.ddp.emit(data.id, { id: data.id, result: data.result, error: data.error })); this.on('ready', data => this.ddp.emit(data.subs[0], data)); + // this.on('error', () => this.reconnect()); + this.on('disconnected', debounce(() => this.reconnect(), 300)); + this.on('logged', () => this._logged = true); + + this.on('logged', () => { + Object.keys(this.subscriptions || {}).forEach((key) => { + const { name, params } = this.subscriptions[key]; + this.subscriptions[key].unsubscribe(); + this.subscribe(name, ...params); + }); + }); + this.on('open', async() => { + this._logged = false; + this.send({ msg: 'connect', version: '1', support: ['1', 'pre2', 'pre1'] }); + }); + + this._connect(); } - send(obj) { + check() { + if (!this.lastping) { + return false; + } + if ((Math.abs(this.lastping.getTime() - new Date().getTime()) / 1000) > 50) { + return false; + } + return true; + } + async login(params) { + try { + this.emit('login', params); + const result = await this.call('login', params); + this._login = { resume: result.token, ...result }; + this._logged = true; + this.emit('logged', result); + return result; + } catch (err) { + const error = { ...err }; + if (/user not found/i.test(error.reason)) { + error.error = 1; + error.reason = 'User or Password incorrect'; + error.message = 'User or Password incorrect'; + } + this.emit('logginError', error); + return Promise.reject(error); + } + } + async send(obj, ignore) { + console.log('send'); return new Promise((resolve, reject) => { this.id += 1; - const id = obj.id || `${ this.id }`; + const id = obj.id || `ddp-react-native-${ this.id }`; + // console.log('send', { ...obj, id }); this.connection.send(EJSON.stringify({ ...obj, id })); - this.ddp.once(id, data => (data.error ? reject(data.error) : resolve({ id, ...data }))); + if (ignore) { + return; + } + const cancel = this.ddp.once('disconnected', reject); + this.ddp.once(id, (data) => { + // console.log(data); + this.ddp.removeListener(id, cancel); + return (data.error ? reject(data.error) : resolve({ id, ...data })); + }); }); } + get status() { + return this.connection && this.connection.readyState === 1 && this.check() && !!this._logged; + } + _close() { + try { + // this.connection && this.connection.readyState > 1 && this.connection.close && this.connection.close(300, 'disconnect'); + if (this.connection && this.connection.close) { + this.connection.close(300, 'disconnect'); + delete this.connection; + } + } catch (e) { + // console.log(e); + } + } _connect() { - const connection = new WebSocket(`${ this.url }/websocket`); - connection.onopen = () => { - this.emit('open'); - this.send({ msg: 'connect', version: '1', support: ['1', 'pre2', 'pre1'] }); - }; - connection.onclose = e => this.emit('disconnected', e); - // connection.onerror = () => { - // // alert(error.type); - // // console.log(error); - // // console.log(`WebSocket Error ${ JSON.stringify({...error}) }`); - // }; + return new Promise((resolve) => { + this.lastping = new Date(); + this._close(); + clearInterval(this.reconnect_timeout); + this.reconnect_timeout = setInterval(() => (!this.connection || this.connection.readyState > 1 || !this.check()) && this.reconnect(), 5000); + this.connection = new WebSocket(`${ this.url }/websocket`, null); - connection.onmessage = (e) => { - const data = EJSON.parse(e.data); - this.emit(data.msg, data); - return data.collection && this.emit(data.collection, data); - }; - // this.on('disconnected', e => alert(JSON.stringify(e))); - this.connection = connection; + this.connection.onopen = () => { + this.emit('open'); + resolve(); + this.ddp.emit('open'); + return this._login && this.login(this._login); + }; + this.connection.onclose = debounce((e) => { console.log('aer'); this.emit('disconnected', e); }, 300); + this.connection.onmessage = (e) => { + try { + // console.log('received', e.data, e.target.readyState); + const data = EJSON.parse(e.data); + this.emit(data.msg, data); + return data.collection && this.emit(data.collection, data); + } catch (err) { + Answers.logCustom('EJSON parse', err); + } + }; + }); } logout() { + this._login = null; return this.call('logout').then(() => this.subscriptions = {}); } disconnect() { - this.emit('disconnected_by_user'); - this.connection.close(); + this._close(); } - reconnect() { - this.disconnect(); - this.once('connected', () => { - Object.keys(this.subscriptions).forEach((key) => { - const { name, params } = this.subscriptions[key]; - this.subscriptions[key].unsubscribe(); - this.subscribe(name, params); - }); - }); - this._connect(); + async reconnect() { + if (this._timer) { + return; + } + delete this.connection; + this._logged = false; + + this._timer = setTimeout(() => { + delete this._timer; + this._connect(); + }, 1000); } call(method, ...params) { return this.send({ msg: 'method', method, params - }).then(data => data.result || data.subs); + }).then(data => data.result || data.subs).catch((err) => { + Answers.logCustom('DDP call Error', err); + return Promise.reject(err); + }); } unsubscribe(id) { if (!this.subscriptions[id]) { @@ -109,19 +253,31 @@ export default class Socket extends EventEmitter { return this.send({ msg: 'unsub', id - }).then(data => data.result || data.subs); + }).then(data => data.result || data.subs).catch((err) => { + console.warn('unsubscribe', err); + Answers.logCustom('DDP unsubscribe Error', err); + return Promise.reject(err); + }); } subscribe(name, ...params) { + console.log(name, params); return this.send({ msg: 'sub', name, params }).then(({ id }) => { const args = { + id, name, params, unsubscribe: () => this.unsubscribe(id) }; + this.subscriptions[id] = args; + // console.log(args); return args; + }).catch((err) => { + console.warn('subscribe', err); + Answers.logCustom('DDP subscribe Error', err); + return Promise.reject(err); }); } } diff --git a/app/lib/methods/getCustomEmojis.js b/app/lib/methods/getCustomEmojis.js new file mode 100644 index 000000000..391b2ed4f --- /dev/null +++ b/app/lib/methods/getCustomEmojis.js @@ -0,0 +1,23 @@ +import { InteractionManager } from 'react-native'; +import reduxStore from '../createStore'; +// import { get } from './helpers/rest'; + +import database from '../realm'; +import * as actions from '../../actions'; + +const getLastMessage = () => { + const setting = database.objects('customEmojis').sorted('_updatedAt', true)[0]; + return setting && setting._updatedAt; +}; + + +export default async function() { + const lastMessage = getLastMessage(); + let emojis = await this.ddp.call('listEmojiCustom'); + emojis = emojis.filter(emoji => !lastMessage || emoji._updatedAt > lastMessage); + emojis = this._prepareEmojis(emojis); + InteractionManager.runAfterInteractions(() => database.write(() => { + emojis.forEach(emoji => database.create('customEmojis', emoji, true)); + })); + reduxStore.dispatch(actions.setCustomEmojis(this.parseEmojis(emojis))); +} diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js new file mode 100644 index 000000000..542276a27 --- /dev/null +++ b/app/lib/methods/getPermissions.js @@ -0,0 +1,22 @@ +import { InteractionManager } from 'react-native'; +import reduxStore from '../createStore'; +// import { get } from './helpers/rest'; + +import database from '../realm'; +import * as actions from '../../actions'; + +const getLastMessage = () => { + const setting = database.objects('permissions').sorted('_updatedAt', true)[0]; + return setting && setting._updatedAt; +}; + + +export default async function() { + const lastMessage = getLastMessage(); + const result = await (!lastMessage ? this.ddp.call('permissions/get') : this.ddp.call('permissions/get', new Date(lastMessage))); + const permissions = this._preparePermissions(result.update || result); + console.log('getPermissions', permissions); + InteractionManager.runAfterInteractions(() => database.write(() => + permissions.forEach(permission => database.create('permissions', permission, true)))); + reduxStore.dispatch(actions.setAllPermissions(this.parsePermissions(permissions))); +} diff --git a/app/lib/methods/getRooms.js b/app/lib/methods/getRooms.js new file mode 100644 index 000000000..ef2732e00 --- /dev/null +++ b/app/lib/methods/getRooms.js @@ -0,0 +1,51 @@ +import { InteractionManager } from 'react-native'; +// import { showToast } from '../../utils/info'; +import { get } from './helpers/rest'; +import mergeSubscriptionsRooms, { merge } from './helpers/mergeSubscriptionsRooms'; +import database from '../realm'; + +const lastMessage = () => { + const message = database + .objects('subscriptions') + .sorted('roomUpdatedAt', true)[0]; + return message && new Date(message.roomUpdatedAt); +}; + +const getRoomRest = async function() { + const { ddp } = this; + const updatedSince = lastMessage(); + const { token, id } = ddp._login; + const server = this.ddp.url.replace('ws', 'http'); + const [subscriptions, rooms] = await Promise.all([get({ token, id, server }, 'subscriptions.get', { updatedSince }), get({ token, id, server }, 'rooms.get', { updatedSince })]); + return mergeSubscriptionsRooms(subscriptions, rooms); +}; + +const getRoomDpp = async function() { + try { + const { ddp } = this; + const updatedSince = lastMessage(); + const [subscriptions, rooms] = await Promise.all([ddp.call('subscriptions/get', updatedSince), ddp.call('rooms/get', updatedSince)]); + return mergeSubscriptionsRooms(subscriptions, rooms); + } catch (e) { + return getRoomRest.apply(this); + } +}; + +export default async function() { + const { database: db } = database; + + return new Promise(async(resolve) => { + // eslint-disable-next-line + const { subscriptions, rooms } = await (false && this.ddp.status ? getRoomDpp.apply(this) : getRoomRest.apply(this)); + + const data = rooms.map(room => ({ room, sub: database.objects('subscriptions').filtered('rid == $0', room._id) })); + + InteractionManager.runAfterInteractions(() => { + db.write(() => { + subscriptions.forEach(subscription => db.create('subscriptions', subscription, true)); + data.forEach(({ sub, room }) => sub[0] && merge(sub[0], room)); + }); + resolve(data); + }); + }); +} diff --git a/app/lib/methods/getSettings.js b/app/lib/methods/getSettings.js new file mode 100644 index 000000000..d2c94acb9 --- /dev/null +++ b/app/lib/methods/getSettings.js @@ -0,0 +1,24 @@ +import { InteractionManager } from 'react-native'; +import reduxStore from '../createStore'; +// import { get } from './helpers/rest'; + +import database from '../realm'; +import * as actions from '../../actions'; + +const getLastMessage = () => { + const [setting] = database.objects('settings').sorted('_updatedAt', true); + return setting && setting._updatedAt; +}; + +export default async function() { + const lastMessage = getLastMessage(); + const result = await (!lastMessage ? this.ddp.call('public-settings/get') : this.ddp.call('public-settings/get', new Date(lastMessage))); + console.log('getSettings', lastMessage, result); + + const filteredSettings = this._prepareSettings(this._filterSettings(result.update || result)); + + InteractionManager.runAfterInteractions(() => + database.write(() => + filteredSettings.forEach(setting => database.create('settings', setting, true)))); + reduxStore.dispatch(actions.addSettings(this.parseSettings(filteredSettings))); +} diff --git a/app/lib/methods/helpers/buildMessage.js b/app/lib/methods/helpers/buildMessage.js new file mode 100644 index 000000000..a565046fa --- /dev/null +++ b/app/lib/methods/helpers/buildMessage.js @@ -0,0 +1,7 @@ +import normalizeMessage from './normalizeMessage'; +import messagesStatus from '../../../constants/messagesStatus'; + +export default (message) => { + message.status = messagesStatus.SENT; + return normalizeMessage(message); +}; diff --git a/app/lib/methods/helpers/mergeSubscriptionsRooms.js b/app/lib/methods/helpers/mergeSubscriptionsRooms.js new file mode 100644 index 000000000..15cc768f1 --- /dev/null +++ b/app/lib/methods/helpers/mergeSubscriptionsRooms.js @@ -0,0 +1,51 @@ +import normalizeMessage from './normalizeMessage'; +// TODO: delete and update + +export const merge = (subscription, room) => { + subscription.muted = []; + if (room) { + subscription.roomUpdatedAt = room._updatedAt; + subscription.lastMessage = normalizeMessage(room.lastMessage); + subscription.ro = room.ro; + subscription.description = room.description; + subscription.topic = room.topic; + subscription.announcement = room.announcement; + subscription.reactWhenReadOnly = room.reactWhenReadOnly; + subscription.archived = room.archived; + subscription.joinCodeRequired = room.joinCodeRequired; + + if (room.muted && room.muted.length) { + subscription.muted = room.muted.filter(role => role).map(role => ({ value: role })); + } + } + if (subscription.roles && subscription.roles.length) { + subscription.roles = subscription.roles.map(role => (role.value ? role : { value: role })); + } + + if (subscription.mobilePushNotifications === 'nothing') { + subscription.notifications = true; + } else { + subscription.notifications = false; + } + + subscription.blocked = !!subscription.blocker; + return subscription; +}; + +export default (subscriptions = [], rooms = []) => { + if (subscriptions.update) { + subscriptions = subscriptions.update; + rooms = rooms.update; + } + return { + subscriptions: subscriptions.map((s) => { + const index = rooms.findIndex(({ _id }) => _id === s.rid); + if (index < 0) { + return merge(s); + } + const [room] = rooms.splice(index, 1); + return merge(s, room); + }), + rooms + }; +}; diff --git a/app/lib/methods/helpers/normalizeMessage.js b/app/lib/methods/helpers/normalizeMessage.js new file mode 100644 index 000000000..e391a7ce2 --- /dev/null +++ b/app/lib/methods/helpers/normalizeMessage.js @@ -0,0 +1,31 @@ +import parseUrls from './parseUrls'; + +function normalizeAttachments(msg) { + if (typeof msg.attachments !== typeof [] || !msg.attachments || !msg.attachments.length) { + msg.attachments = []; + } + msg.attachments = msg.attachments.map((att) => { + att.fields = att.fields || []; + att = normalizeAttachments(att); + return att; + }); + return msg; +} + +export default (msg) => { + if (!msg) { return; } + msg = normalizeAttachments(msg); + msg.reactions = msg.reactions || []; + // TODO: api problems + if (Array.isArray(msg.reactions)) { + msg.reactions = msg.reactions.map((value, key) => ({ teste: 1, emoji: key, usernames: value.usernames.map(username => ({ value: username })) })); + } else { + msg.reactions = Object.keys(msg.reactions).map(key => ({ teste: 1, emoji: key, usernames: msg.reactions[key].usernames.map(username => ({ value: username })) })); + } + msg.urls = msg.urls ? parseUrls(msg.urls) : []; + msg._updatedAt = new Date(); + // loadHistory returns msg.starred as object + // stream-room-msgs returns msg.starred as an array + msg.starred = msg.starred && (Array.isArray(msg.starred) ? msg.starred.length > 0 : !!msg.starred); + return msg; +}; diff --git a/app/lib/methods/helpers/parseUrls.js b/app/lib/methods/helpers/parseUrls.js new file mode 100644 index 000000000..e0896e3a7 --- /dev/null +++ b/app/lib/methods/helpers/parseUrls.js @@ -0,0 +1,14 @@ +export default urls => urls.filter(url => url.meta && !url.ignoreParse).map((url, index) => { + const tmp = {}; + const { meta } = url; + tmp._id = index; + tmp.title = meta.ogTitle || meta.twitterTitle || meta.title || meta.pageTitle || meta.oembedTitle; + tmp.description = meta.ogDescription || meta.twitterDescription || meta.description || meta.oembedAuthorName; + let decodedOgImage; + if (meta.ogImage) { + decodedOgImage = meta.ogImage.replace(/&/g, '&'); + } + tmp.image = decodedOgImage || meta.twitterImage || meta.oembedThumbnailUrl; + tmp.url = url.url; + return tmp; +}); diff --git a/app/lib/methods/helpers/protectedFunction.js b/app/lib/methods/helpers/protectedFunction.js new file mode 100644 index 000000000..f44fe6f51 --- /dev/null +++ b/app/lib/methods/helpers/protectedFunction.js @@ -0,0 +1,12 @@ +import { Answers } from 'react-native-fabric'; + +export default fn => (params) => { + try { + fn(params); + } catch (e) { + Answers.logCustom('erro', e); + if (__DEV__) { + alert(e); + } + } +}; diff --git a/app/lib/methods/helpers/rest.js b/app/lib/methods/helpers/rest.js new file mode 100644 index 000000000..1efe08f54 --- /dev/null +++ b/app/lib/methods/helpers/rest.js @@ -0,0 +1,40 @@ +import toQuery from './toQuery'; + + +const handleSuccess = (msg) => { + if (msg.success !== undefined && !msg.success) { + return Promise.reject(msg); + } + return msg; +}; + +export const get = function({ + token, id, server +}, method, params = {}) { + return fetch(`${ server }/api/v1/${ method }/?${ toQuery(params) }`, { + method: 'get', + headers: { + // 'Accept-Encoding': 'gzip', + 'Content-Type': 'application/json', + 'X-Auth-Token': token, + 'X-User-Id': id + } + }).then(response => response.json()).then(handleSuccess); +}; + + +export const post = function({ + token, id, server +}, method, params = {}) { + return fetch(`${ server }/api/v1/${ method }`, { + method: 'post', + body: JSON.stringify(params), + headers: { + // 'Accept-Encoding': 'gzip', + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-Auth-Token': token, + 'X-User-Id': id + } + }).then(response => response.json()).then(handleSuccess); +}; diff --git a/app/lib/methods/helpers/toQuery.js b/app/lib/methods/helpers/toQuery.js new file mode 100644 index 000000000..4fb9b7776 --- /dev/null +++ b/app/lib/methods/helpers/toQuery.js @@ -0,0 +1,3 @@ +export default function(obj) { + return Object.keys(obj).filter(p => obj[p] !== undefined && obj[p] !== null).map(p => `${ encodeURIComponent(p) }=${ encodeURIComponent(obj[p]) }`).join('&'); +} diff --git a/app/lib/methods/loadMessagesForRoom.js b/app/lib/methods/loadMessagesForRoom.js new file mode 100644 index 000000000..9f12a93ea --- /dev/null +++ b/app/lib/methods/loadMessagesForRoom.js @@ -0,0 +1,68 @@ +import { InteractionManager } from 'react-native'; + +import { get } from './helpers/rest'; +import buildMessage from './helpers/buildMessage'; +import database from '../realm'; + + +// TODO: api fix +const types = { + c: 'channels', d: 'im', p: 'groups' +}; + +async function loadMessagesForRoomRest({ rid: roomId, latest, t }) { + const { token, id } = this.ddp._login; + const server = this.ddp.url.replace('ws', 'http'); + const data = await get({ token, id, server }, `${ types[t] }.history`, { roomId, latest }); + return data.messages; +} + +async function loadMessagesForRoomDDP(...args) { + const [{ rid: roomId, latest }] = args; + try { + const data = await this.ddp.call('loadHistory', roomId, latest, 50); + if (!data || !data.messages.length) { + return []; + } + return data.messages; + } catch (e) { + console.warn('loadMessagesForRoomDDP', e); + return loadMessagesForRoomRest.call(this, ...args); + } + + // } + // if (cb) { + // cb({ end: data && data.messages.length < 20 }); + // } + // return data.message; + // }, (err) => { + // if (err) { + // if (cb) { + // cb({ end: true }); + // } + // return Promise.reject(err); + // } + // }); +} + +export default async function loadMessagesForRoom(...args) { + console.log('aqui'); + const { database: db } = database; + console.log('database', db); + + return new Promise(async(resolve) => { + // eslint-disable-next-line + const data = (await (false && this.ddp.status ? loadMessagesForRoomDDP.call(this, ...args) : loadMessagesForRoomRest.call(this, ...args))).map(buildMessage); + if (data) { + InteractionManager.runAfterInteractions(() => { + try { + db.write(() => data.forEach(message => db.create('messages', message, true))); + resolve(data); + } catch (e) { + console.warn('loadMessagesForRoom', e); + } + }); + } + return resolve([]); + }); +} diff --git a/app/lib/methods/loadMissedMessages.js b/app/lib/methods/loadMissedMessages.js new file mode 100644 index 000000000..5bd0074c1 --- /dev/null +++ b/app/lib/methods/loadMissedMessages.js @@ -0,0 +1,60 @@ +import { InteractionManager } from 'react-native'; + +import { get } from './helpers/rest'; +import buildMessage from './helpers/buildMessage'; +import database from '../realm'; + + +async function loadMissedMessagesRest({ rid: roomId, lastOpen: lastUpdate }) { + const { token, id } = this.ddp._login; + const server = this.ddp.url.replace('ws', 'http'); + const { result } = await get({ token, id, server }, 'chat.syncMessages', { roomId, lastUpdate }); + // TODO: api fix + return result.updated || result.messages; +} + +async function loadMissedMessagesDDP(...args) { + const [{ rid, lastOpen: lastUpdate }] = args; + + try { + const data = await this.ddp.call('messages/get', rid, { lastUpdate: new Date(lastUpdate) }); + return data.updated || data.messages; + } catch (e) { + return loadMissedMessagesRest.call(this, ...args); + } + + // } + // if (cb) { + // cb({ end: data && data.messages.length < 20 }); + // } + // return data.message; + // }, (err) => { + // if (err) { + // if (cb) { + // cb({ end: true }); + // } + // return Promise.reject(err); + // } + // }); +} + +export default async function(...args) { + const { database: db } = database; + return new Promise(async(resolve) => { + // eslint-disable-next-line + const data = (await (false && this.ddp.status ? loadMissedMessagesDDP.call(this, ...args) : loadMissedMessagesRest.call(this, ...args))); + + if (data) { + data.forEach(buildMessage); + return InteractionManager.runAfterInteractions(() => { + try { + db.write(() => data.forEach(message => db.create('messages', message, true))); + resolve(data); + } catch (e) { + console.warn('loadMessagesForRoom', e); + } + }); + } + resolve([]); + }); +} diff --git a/app/lib/methods/readMessages.js b/app/lib/methods/readMessages.js new file mode 100644 index 000000000..10ac99410 --- /dev/null +++ b/app/lib/methods/readMessages.js @@ -0,0 +1,33 @@ +import { post } from './helpers/rest'; +import database from '../realm'; + +const readMessagesREST = function readMessagesREST(rid) { + const { token, id } = this.ddp._login; + const server = this.ddp.url.replace('ws', 'http'); + return post({ token, id, server }, 'subscriptions.read', { rid }); +}; + +const readMessagesDDP = function readMessagesDDP(rid) { + try { + return this.ddp.call('readMessages', rid); + } catch (e) { + return readMessagesREST.call(this, rid); + } +}; + +export default async function readMessages(rid) { + const { database: db } = database; + // eslint-disable-next-line + const data = await (false && this.ddp.status ? readMessagesDDP.call(this, rid) : readMessagesREST.call(this, rid)); + const [subscription] = db.objects('subscriptions').filtered('rid = $0', rid); + db.write(() => { + subscription.open = true; + subscription.alert = false; + subscription.unread = 0; + subscription.userMentions = 0; + subscription.groupMentions = 0; + subscription.ls = new Date(); + subscription.lastOpen = new Date(); + }); + return data; +} diff --git a/app/lib/methods/sendMessage.js b/app/lib/methods/sendMessage.js new file mode 100644 index 000000000..eba1f5eac --- /dev/null +++ b/app/lib/methods/sendMessage.js @@ -0,0 +1,72 @@ +import Random from 'react-native-meteor/lib/Random'; +import messagesStatus from '../../constants/messagesStatus'; + +import buildMessage from '../methods/helpers/buildMessage'; +import { post } from './helpers/rest'; +import database from '../realm'; +import reduxStore from '../createStore'; + +export const getMessage = (rid, msg = {}) => { + const _id = Random.id(); + const message = { + _id, + rid, + msg, + ts: new Date(), + _updatedAt: new Date(), + status: messagesStatus.TEMP, + u: { + _id: reduxStore.getState().login.user.id || '1', + username: reduxStore.getState().login.user.username + } + }; + database.write(() => { + database.create('messages', message, true); + }); + return message; +}; + +function sendMessageByRest(message) { + const { token, id } = this.ddp._login; + const server = this.ddp.url.replace('ws', 'http'); + const { _id, rid, msg } = message; + return post({ token, id, server }, 'chat.sendMessage', { message: { _id, rid, msg } }); +} + +function sendMessageByDDP(message) { + const { _id, rid, msg } = message; + return this.ddp.call('sendMessage', { _id, rid, msg }); +} + +export async function _sendMessageCall(message) { + try { + // eslint-disable-next-line + const data = await (false && this.ddp.status ? sendMessageByDDP.call(this, message) : sendMessageByRest.call(this, message)); + return data; + } catch (e) { + database.write(() => { + message.status = messagesStatus.ERROR; + database.create('messages', message, true); + }); + } +} + +export default async function(rid, msg) { + const { database: db } = database; + try { + const message = getMessage(rid, msg); + const room = db.objects('subscriptions').filtered('rid == $0', rid); + + db.write(() => { + room.lastMessage = message; + }); + + const ret = await _sendMessageCall.call(this, message); + // TODO: maybe I have created a bug in the future here <3 + db.write(() => { + db.create('messages', buildMessage({ ...message, ...ret }), true); + }); + } catch (e) { + console.warn('sendMessage', e); + } +} diff --git a/app/lib/methods/subscriptions/room.js b/app/lib/methods/subscriptions/room.js new file mode 100644 index 000000000..6c045e6b8 --- /dev/null +++ b/app/lib/methods/subscriptions/room.js @@ -0,0 +1,71 @@ +// import database from '../../realm'; +// import reduxStore from '../../createStore'; +// import normalizeMessage from '../helpers/normalizeMessage'; +// import _buildMessage from '../helpers/buildMessage'; +// import protectedFunction from '../helpers/protectedFunction'; + +const subscribe = (ddp, rid) => Promise.all([ + ddp.subscribe('stream-room-messages', rid, false), + ddp.subscribe('stream-notify-room', `${ rid }/typing`, false) +]); +const unsubscribe = subscriptions => subscriptions.forEach(sub => sub.unsubscribe().catch(e => console.warn(e))); + +let timer = null; +let promises; +let logged; +let disconnected; + +const stop = (ddp) => { + if (promises) { + promises.then(unsubscribe); + promises = false; + } + + ddp.removeListener('logged', logged); + ddp.removeListener('disconnected', disconnected); + + logged = false; + disconnected = false; + + clearTimeout(timer); +}; + +export default async function subscribeRoom({ rid, t }) { + if (promises) { + promises.then(unsubscribe); + promises = false; + } + const loop = (time = new Date()) => { + if (timer) { + return; + } + timer = setTimeout(async() => { + try { + await this.loadMissedMessages({ rid, t, lastOpen: timer }); + timer = false; + loop(); + } catch (e) { + loop(time); + } + }, 5000); + }; + + + logged = this.ddp.on('logged', () => { + clearTimeout(timer); + timer = false; + promises = subscribe(this.ddp, rid); + }); + + disconnected = this.ddp.on('disconnected', () => { loop(); }); + + if (!this.ddp.status) { + loop(); + } else { + promises = subscribe(this.ddp, rid); + } + + return { + stop: () => stop(this.ddp) + }; +} diff --git a/app/lib/methods/subscriptions/rooms.js b/app/lib/methods/subscriptions/rooms.js new file mode 100644 index 000000000..4302568b2 --- /dev/null +++ b/app/lib/methods/subscriptions/rooms.js @@ -0,0 +1,58 @@ +import database from '../../realm'; +import { merge } from '../helpers/mergeSubscriptionsRooms'; + +export default async function subscribeRooms(id) { + const subscriptions = Promise.all([ + this.ddp.subscribe('stream-notify-user', `${ id }/subscriptions-changed`, false), + this.ddp.subscribe('stream-notify-user', `${ id }/rooms-changed`, false), + this.ddp.subscribe('stream-notify-user', `${ id }/message`, false) + ]); + + let timer = null; + const loop = (time = new Date()) => { + if (timer) { + return; + } + timer = setTimeout(async() => { + try { + await this.getRooms(time); + timer = false; + loop(); + } catch (e) { + loop(time); + } + }, 5000); + }; + + this.ddp.on('logged', () => { + clearTimeout(timer); + timer = false; + }); + + this.ddp.on('logout', () => { + clearTimeout(timer); + timer = true; + }); + + this.ddp.on('disconnected', () => { loop(); }); + + this.ddp.on('stream-notify-user', (ddpMessage) => { + const [type, data] = ddpMessage.fields.args; + const [, ev] = ddpMessage.fields.eventName.split('/'); + if (/subscriptions/.test(ev)) { + const tpm = merge(data); + return database.write(() => { + database.create('subscriptions', tpm, true); + }); + } + if (/rooms/.test(ev) && type === 'updated') { + const [sub] = database.objects('subscriptions').filtered('rid == $0', data._id); + database.write(() => { + merge(sub, data); + }); + } + }); + + await subscriptions; + console.log(this.ddp.subscriptions); +} diff --git a/app/lib/realm.js b/app/lib/realm.js index 8ad8d5db7..bfe84ed59 100644 --- a/app/lib/realm.js +++ b/app/lib/realm.js @@ -51,6 +51,7 @@ const roomsSchema = { _id: 'string', t: 'string', lastMessage: 'messages', + description: { type: 'string', optional: true }, _updatedAt: { type: 'date', optional: true } } }; @@ -63,6 +64,14 @@ const subscriptionRolesSchema = { } }; +const userMutedInRoomSchema = { + name: 'usersMuted', + primaryKey: 'value', + properties: { + value: 'string' + } +}; + const subscriptionSchema = { name: 'subscriptions', primaryKey: '_id', @@ -90,7 +99,9 @@ const subscriptionSchema = { blocked: { type: 'bool', optional: true }, reactWhenReadOnly: { type: 'bool', optional: true }, archived: { type: 'bool', optional: true }, - joinCodeRequired: { type: 'bool', optional: true } + joinCodeRequired: { type: 'bool', optional: true }, + notifications: { type: 'bool', optional: true }, + muted: { type: 'list', objectType: 'usersMuted' } } }; @@ -137,7 +148,9 @@ const attachment = { color: { type: 'string', optional: true }, ts: { type: 'date', optional: true }, attachments: { type: 'list', objectType: 'attachment' }, - fields: { type: 'list', objectType: 'attachmentFields' } + fields: { + type: 'list', objectType: 'attachmentFields', default: [] + } } }; @@ -265,8 +278,48 @@ const schema = [ customEmojisSchema, messagesReactionsSchema, messagesReactionsUsernamesSchema, - rolesSchema + rolesSchema, + userMutedInRoomSchema ]; + +// class DebouncedDb { +// constructor(db) { +// this.database = db; +// } +// deleteAll(...args) { +// return this.database.write(() => this.database.deleteAll(...args)); +// } +// delete(...args) { +// return this.database.delete(...args); +// } +// write(fn) { +// return fn(); +// } +// create(...args) { +// this.queue = this.queue || []; +// if (this.timer) { +// clearTimeout(this.timer); +// this.timer = null; +// } +// this.timer = setTimeout(() => { +// alert(this.queue.length); +// this.database.write(() => { +// this.queue.forEach(({ db, args }) => this.database.create(...args)); +// }); +// +// this.timer = null; +// return this.roles = []; +// }, 1000); +// +// this.queue.push({ +// db: this.database, +// args +// }); +// } +// objects(...args) { +// return this.database.objects(...args); +// } +// } class DB { databases = { serversDB: new Realm({ @@ -296,7 +349,7 @@ class DB { return this.databases.activeDB; } - setActiveDB(database) { + setActiveDB(database = '') { const path = database.replace(/(^\w+:|^)\/\//, ''); return this.databases.activeDB = new Realm({ path: `${ path }.realm`, diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 97b00d3b9..834c9dd98 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -1,47 +1,56 @@ -import Random from 'react-native-meteor/lib/Random'; import { AsyncStorage, Platform } from 'react-native'; import { hashPassword } from 'react-native-meteor/lib/utils'; -import _ from 'lodash'; +import foreach from 'lodash/forEach'; +import Random from 'react-native-meteor/lib/Random'; +import { Answers } from 'react-native-fabric'; import RNFetchBlob from 'react-native-fetch-blob'; import reduxStore from './createStore'; import settingsType from '../constants/settings'; import messagesStatus from '../constants/messagesStatus'; import database from './realm'; -import * as actions from '../actions'; -import { someoneTyping, roomMessageReceived } from '../actions/room'; -import { setUser, setLoginServices, removeLoginServices } from '../actions/login'; -import { disconnect, disconnect_by_user, connectSuccess, connectFailure } from '../actions/connect'; +// import * as actions from '../actions'; + +import { setUser, setLoginServices, removeLoginServices, loginRequest, loginSuccess, loginFailure } from '../actions/login'; +import { disconnect, connectSuccess, connectFailure } from '../actions/connect'; import { setActiveUser } from '../actions/activeUsers'; import { starredMessagesReceived, starredMessageUnstarred } from '../actions/starredMessages'; import { pinnedMessagesReceived, pinnedMessageUnpinned } from '../actions/pinnedMessages'; import { mentionedMessagesReceived } from '../actions/mentionedMessages'; import { snippetedMessagesReceived } from '../actions/snippetedMessages'; import { roomFilesReceived } from '../actions/roomFiles'; +import { someoneTyping, roomMessageReceived } from '../actions/room'; import { setRoles } from '../actions/roles'; import Ddp from './ddp'; -export { Accounts } from 'react-native-meteor'; +import normalizeMessage from './methods/helpers/normalizeMessage'; + +import subscribeRooms from './methods/subscriptions/rooms'; +import subscribeRoom from './methods/subscriptions/room'; + +import protectedFunction from './methods/helpers/protectedFunction'; +import readMessages from './methods/readMessages'; +import getSettings from './methods/getSettings'; + +import getRooms from './methods/getRooms'; +import getPermissions from './methods/getPermissions'; +import getCustomEmoji from './methods/getCustomEmojis'; + + +import _buildMessage from './methods/helpers/buildMessage'; +import loadMessagesForRoom from './methods/loadMessagesForRoom'; +import loadMissedMessages from './methods/loadMissedMessages'; + +import sendMessage, { getMessage, _sendMessageCall } from './methods/sendMessage'; -const call = (method, ...params) => RocketChat.ddp.call(method, ...params); // eslint-disable-line const TOKEN_KEY = 'reactnativemeteor_usertoken'; -const SERVER_TIMEOUT = 30000; - +const call = (method, ...params) => RocketChat.ddp.call(method, ...params); // eslint-disable-line const returnAnArray = obj => obj || []; -const normalizeMessage = (lastMessage) => { - if (lastMessage) { - lastMessage.attachments = lastMessage.attachments || []; - lastMessage.reactions = _.map(lastMessage.reactions, (value, key) => - ({ emoji: key, usernames: value.usernames.map(username => ({ value: username })) })); - } - return lastMessage; -}; - - const RocketChat = { TOKEN_KEY, - + subscribeRooms, + subscribeRoom, createChannel({ name, users, type }) { return call(type ? 'createChannel' : 'createPrivateGroup', name, users, type); }, @@ -97,59 +106,78 @@ const RocketChat = { reduxStore.dispatch(setActiveUser(this.activeUsers)); this._setUserTimer = null; return this.activeUsers = {}; - }, 3000); + }, 1000); + this.activeUsers[ddpMessage.id] = ddpMessage.fields; }, - reconnect() { - if (this.ddp) { - this.ddp.reconnect(); + async loginSuccess(user) { + if (!user) { + const { user: u } = reduxStore.getState().login; + user = Object.assign({}, u); } + + // TODO: one api call + // call /me only one time + if (!user.username) { + const me = await this.me({ token: user.token, userId: user.id }); + // eslint-disable-next-line + user.username = me.username; + } + if (user.username) { + const userInfo = await this.userInfo({ token: user.token, userId: user.id }); + user.username = userInfo.user.username; + if (userInfo.user.roles) { + user.roles = userInfo.user.roles; + } + } + return reduxStore.dispatch(loginSuccess(user)); }, - connect(url) { - if (this.ddp) { - this.ddp.disconnect(); - } - this.ddp = new Ddp(url); + connect(url, login) { return new Promise((resolve) => { - this.ddp.on('disconnected_by_user', () => { - reduxStore.dispatch(disconnect_by_user()); - }); - this.ddp.on('disconnected', () => { + if (this.ddp) { + this.ddp.disconnect(); + delete this.ddp; + } + + this.ddp = new Ddp(url, login); + if (login) { + protectedFunction(() => RocketChat.getRooms()); + } + + this.ddp.on('login', protectedFunction(() => reduxStore.dispatch(loginRequest()))); + + this.ddp.on('logginError', protectedFunction(err => reduxStore.dispatch(loginFailure(err)))); + + this.ddp.on('users', protectedFunction(ddpMessage => RocketChat._setUser(ddpMessage))); + + this.ddp.on('background', () => this.getRooms().catch(e => console.warn('background getRooms', e))); + + this.ddp.on('disconnected', () => console.log('disconnected')); + + this.ddp.on('logged', protectedFunction((user) => { + this.getRooms().catch(e => console.warn('logged getRooms', e)); + this.loginSuccess(user); + })); + this.ddp.once('logged', protectedFunction(({ id }) => { this.subscribeRooms(id); })); + + this.ddp.on('disconnected', protectedFunction(() => { reduxStore.dispatch(disconnect()); - }); - // this.ddp.on('open', async() => { - // resolve(reduxStore.dispatch(connectSuccess())); - // }); - this.ddp.on('connected', () => { - resolve(reduxStore.dispatch(connectSuccess())); - RocketChat.getSettings(); - RocketChat.getPermissions(); - RocketChat.getCustomEmoji(); - this.ddp.subscribe('activeUsers'); - this.ddp.subscribe('roles'); - }); - - this.ddp.on('error', (err) => { - alert(JSON.stringify(err)); - reduxStore.dispatch(connectFailure()); - }); - - this.ddp.on('users', ddpMessage => RocketChat._setUser(ddpMessage)); + })); this.ddp.on('stream-room-messages', (ddpMessage) => { - const message = this._buildMessage(ddpMessage.fields.args[0]); - return reduxStore.dispatch(roomMessageReceived(message)); + const message = _buildMessage(ddpMessage.fields.args[0]); + requestAnimationFrame(() => reduxStore.dispatch(roomMessageReceived(message))); }); - this.ddp.on('stream-notify-room', (ddpMessage) => { + this.ddp.on('stream-notify-room', protectedFunction((ddpMessage) => { const [_rid, ev] = ddpMessage.fields.eventName.split('/'); if (ev !== 'typing') { return; } return reduxStore.dispatch(someoneTyping({ _rid, username: ddpMessage.fields.args[0], typing: ddpMessage.fields.args[1] })); - }); + })); - this.ddp.on('stream-notify-user', (ddpMessage) => { + this.ddp.on('stream-notify-user', protectedFunction((ddpMessage) => { const [type, data] = ddpMessage.fields.args; const [, ev] = ddpMessage.fields.eventName.split('/'); if (/subscriptions/.test(ev)) { @@ -161,6 +189,11 @@ const RocketChat = { } else { data.blocked = false; } + if (data.mobilePushNotifications === 'nothing') { + data.notifications = true; + } else { + data.notifications = false; + } database.write(() => { database.create('subscriptions', data, true); }); @@ -178,11 +211,33 @@ const RocketChat = { sub.reactWhenReadOnly = data.reactWhenReadOnly; sub.archived = data.archived; sub.joinCodeRequired = data.joinCodeRequired; + if (data.muted) { + sub.muted = data.muted.map(m => ({ value: m })); + } }); } - }); + if (/message/.test(ev)) { + const [args] = ddpMessage.fields.args; + const _id = Random.id(); + const message = { + _id, + rid: args.rid, + msg: args.msg, + ts: new Date(), + _updatedAt: new Date(), + status: messagesStatus.SENT, + u: { + _id, + username: 'rocket.cat' + } + }; + requestAnimationFrame(() => database.write(() => { + database.create('messages', message, true); + })); + } + })); - this.ddp.on('rocketchat_starred_message', (ddpMessage) => { + this.ddp.on('rocketchat_starred_message', protectedFunction((ddpMessage) => { if (ddpMessage.msg === 'added') { this.starredMessages = this.starredMessages || []; @@ -191,14 +246,14 @@ const RocketChat = { this.starredMessagesTimer = null; } - this.starredMessagesTimer = setTimeout(() => { + this.starredMessagesTimer = setTimeout(protectedFunction(() => { reduxStore.dispatch(starredMessagesReceived(this.starredMessages)); this.starredMessagesTimer = null; return this.starredMessages = []; - }, 1000); + }), 1000); const message = ddpMessage.fields; message._id = ddpMessage.id; - const starredMessage = this._buildMessage(message); + const starredMessage = _buildMessage(message); this.starredMessages = [...this.starredMessages, starredMessage]; } if (ddpMessage.msg === 'removed') { @@ -206,9 +261,9 @@ const RocketChat = { return reduxStore.dispatch(starredMessageUnstarred(ddpMessage.id)); } } - }); + })); - this.ddp.on('rocketchat_pinned_message', (ddpMessage) => { + this.ddp.on('rocketchat_pinned_message', protectedFunction((ddpMessage) => { if (ddpMessage.msg === 'added') { this.pinnedMessages = this.pinnedMessages || []; @@ -224,7 +279,7 @@ const RocketChat = { }, 1000); const message = ddpMessage.fields; message._id = ddpMessage.id; - const pinnedMessage = this._buildMessage(message); + const pinnedMessage = _buildMessage(message); this.pinnedMessages = [...this.pinnedMessages, pinnedMessage]; } if (ddpMessage.msg === 'removed') { @@ -232,9 +287,9 @@ const RocketChat = { return reduxStore.dispatch(pinnedMessageUnpinned(ddpMessage.id)); } } - }); + })); - this.ddp.on('rocketchat_mentioned_message', (ddpMessage) => { + this.ddp.on('rocketchat_mentioned_message', protectedFunction((ddpMessage) => { if (ddpMessage.msg === 'added') { this.mentionedMessages = this.mentionedMessages || []; @@ -250,12 +305,12 @@ const RocketChat = { }, 1000); const message = ddpMessage.fields; message._id = ddpMessage.id; - const mentionedMessage = this._buildMessage(message); + const mentionedMessage = _buildMessage(message); this.mentionedMessages = [...this.mentionedMessages, mentionedMessage]; } - }); + })); - this.ddp.on('rocketchat_snippeted_message', (ddpMessage) => { + this.ddp.on('rocketchat_snippeted_message', protectedFunction((ddpMessage) => { if (ddpMessage.msg === 'added') { this.snippetedMessages = this.snippetedMessages || []; @@ -271,12 +326,12 @@ const RocketChat = { }, 1000); const message = ddpMessage.fields; message._id = ddpMessage.id; - const snippetedMessage = this._buildMessage(message); + const snippetedMessage = _buildMessage(message); this.snippetedMessages = [...this.snippetedMessages, snippetedMessage]; } - }); + })); - this.ddp.on('room_files', (ddpMessage) => { + this.ddp.on('room_files', protectedFunction((ddpMessage) => { if (ddpMessage.msg === 'added') { this.roomFiles = this.roomFiles || []; @@ -318,9 +373,9 @@ const RocketChat = { } this.roomFiles = [...this.roomFiles, message]; } - }); + })); - this.ddp.on('meteor_accounts_loginServiceConfiguration', (ddpMessage) => { + this.ddp.on('meteor_accounts_loginServiceConfiguration', protectedFunction((ddpMessage) => { if (ddpMessage.msg === 'added') { this.loginServices = this.loginServices || {}; if (this.loginServiceTimer) { @@ -340,9 +395,9 @@ const RocketChat = { } this.loginServiceTimer = setTimeout(() => reduxStore.dispatch(removeLoginServices()), 1000); } - }); + })); - this.ddp.on('rocketchat_roles', (ddpMessage) => { + this.ddp.on('rocketchat_roles', protectedFunction((ddpMessage) => { this.roles = this.roles || {}; if (this.roleTimer) { @@ -353,39 +408,38 @@ const RocketChat = { reduxStore.dispatch(setRoles(this.roles)); database.write(() => { - _.forEach(this.roles, (description, _id) => { + foreach(this.roles, (description, _id) => { database.create('roles', { _id, description }, true); }); }); this.roleTimer = null; return this.roles = {}; - }, 5000); - this.roles[ddpMessage.id] = ddpMessage.fields.description; - }); - }).catch(console.log); - }, + }, 1000); + this.roles[ddpMessage.id] = (ddpMessage.fields && ddpMessage.fields.description) || undefined; + })); - me({ server, token, userId }) { - return fetch(`${ server }/api/v1/me`, { - method: 'get', - headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': token, - 'X-User-Id': userId - } - }).then(response => response.json()); - }, + this.ddp.on('error', protectedFunction((err) => { + console.warn('onError', JSON.stringify(err)); + Answers.logCustom('disconnect', err); + reduxStore.dispatch(connectFailure()); + })); - userInfo({ server, token, userId }) { - return fetch(`${ server }/api/v1/users.info?userId=${ userId }`, { - method: 'get', - headers: { - 'Content-Type': 'application/json', - 'X-Auth-Token': token, - 'X-User-Id': userId - } - }).then(response => response.json()); + // TODO: fix api (get emojis by date/version....) + + this.ddp.on('open', protectedFunction(() => { + RocketChat.getSettings(); + RocketChat.getPermissions(); + reduxStore.dispatch(connectSuccess()); + resolve(); + })); + + this.ddp.once('open', protectedFunction(() => { + this.ddp.subscribe('activeUsers'); + this.ddp.subscribe('roles'); + RocketChat.getCustomEmoji(); + })); + }).catch(err => console.warn(`asd ${ err }`)); }, register({ credentials }) { @@ -442,19 +496,18 @@ const RocketChat = { return this.login(params, callback); }, - loadSubscriptions(cb) { - this.ddp.call('subscriptions/get').then((data) => { - if (data.length) { - database.write(() => { - data.forEach((subscription) => { - database.create('subscriptions', subscription, true); - }); - }); - } - - return cb && cb(); - }); + login(params) { + return this.ddp.login(params); }, + logout({ server }) { + if (this.ddp) { + this.ddp.logout(); + } + database.deleteAll(); + AsyncStorage.removeItem(TOKEN_KEY); + AsyncStorage.removeItem(`${ TOKEN_KEY }-${ server }`); + }, + registerPushToken(id, token) { const key = Platform.OS === 'ios' ? 'apn' : 'gcm'; const data = { @@ -470,92 +523,32 @@ const RocketChat = { updatePushToken(pushId) { return call('raix:push-setuser', pushId); }, - - _parseUrls(urls) { - return urls.filter(url => url.meta && !url.ignoreParse).map((url, index) => { - const tmp = {}; - const { meta } = url; - tmp._id = index; - tmp.title = meta.ogTitle || meta.twitterTitle || meta.title || meta.pageTitle || meta.oembedTitle; - tmp.description = meta.ogDescription || meta.twitterDescription || meta.description || meta.oembedAuthorName; - let decodedOgImage; - if (meta.ogImage) { - decodedOgImage = meta.ogImage.replace(/&/g, '&'); + loadMissedMessages, + loadMessagesForRoom, + getMessage, + sendMessage, + getRooms, + readMessages, + me({ server = reduxStore.getState().server.server, token, userId }) { + return fetch(`${ server }/api/v1/me`, { + method: 'get', + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Token': token, + 'X-User-Id': userId } - tmp.image = decodedOgImage || meta.twitterImage || meta.oembedThumbnailUrl; - tmp.url = url.url; - return tmp; - }); - }, - _buildMessage(message) { - message.status = messagesStatus.SENT; - normalizeMessage(message); - message.urls = message.urls ? RocketChat._parseUrls(message.urls) : []; - message._updatedAt = new Date(); - // loadHistory returns message.starred as object - // stream-room-messages returns message.starred as an array - message.starred = message.starred && (Array.isArray(message.starred) ? message.starred.length > 0 : !!message.starred); - return message; - }, - loadMessagesForRoom(rid, end, cb) { - return this.ddp.call('loadHistory', rid, end, 20).then((data) => { - if (data && data.messages.length) { - const messages = data.messages.map(message => this._buildMessage(message)); - database.write(() => { - messages.forEach((message) => { - database.create('messages', message, true); - }); - }); - } - if (cb) { - cb({ end: data && data.messages.length < 20 }); - } - return data.message; - }, (err) => { - if (err) { - if (cb) { - cb({ end: true }); - } - return Promise.reject(err); - } - }); + }).then(response => response.json()); }, - getMessage(rid, msg = {}) { - const _id = Random.id(); - const message = { - _id, - rid, - msg, - ts: new Date(), - _updatedAt: new Date(), - status: messagesStatus.TEMP, - u: { - _id: reduxStore.getState().login.user.id || '1', - username: reduxStore.getState().login.user.username + userInfo({ server = reduxStore.getState().server.server, token, userId }) { + return fetch(`${ server }/api/v1/users.info?userId=${ userId }`, { + method: 'get', + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Token': token, + 'X-User-Id': userId } - }; - - database.write(() => { - database.create('messages', message, true); - }); - return message; - }, - async _sendMessageCall(message) { - const { _id, rid, msg } = message; - const sendMessageCall = call('sendMessage', { _id, rid, msg }); - const timeoutCall = new Promise(resolve => setTimeout(resolve, SERVER_TIMEOUT, 'timeout')); - const result = await Promise.race([sendMessageCall, timeoutCall]); - if (result === 'timeout') { - database.write(() => { - message.status = messagesStatus.ERROR; - database.create('messages', message, true); - }); - } - }, - async sendMessage(rid, msg) { - const tempMessage = this.getMessage(rid, msg); - return RocketChat._sendMessageCall(tempMessage); + }).then(response => response.json()); }, async resendMessage(messageId) { const message = await database.objects('messages').filtered('_id = $0', messageId)[0]; @@ -563,7 +556,7 @@ const RocketChat = { message.status = messagesStatus.TEMP; database.create('messages', message, true); }); - return RocketChat._sendMessageCall(message); + return _sendMessageCall(JSON.parse(JSON.stringify(message))); }, spotlight(search, usernames, type) { @@ -573,16 +566,6 @@ const RocketChat = { createDirectMessage(username) { return call('createDirectMessage', username); }, - async readMessages(rid) { - const ret = await call('readMessages', rid); - - const [subscription] = database.objects('subscriptions').filtered('rid = $0', rid); - database.write(() => { - subscription.lastOpen = new Date(); - }); - - return ret; - }, joinRoom(rid) { return call('joinRoom', rid); }, @@ -646,6 +629,7 @@ const RocketChat = { } catch (e) { return e; } finally { + // TODO: fix that try { database.write(() => { const msg = database.objects('messages').filtered('_id = $0', placeholder._id); @@ -656,93 +640,9 @@ const RocketChat = { } } }, - async getRooms() { - const { login } = reduxStore.getState(); - let lastMessage = database - .objects('subscriptions') - .sorted('roomUpdatedAt', true)[0]; - lastMessage = lastMessage && new Date(lastMessage.roomUpdatedAt); - let [subscriptions, rooms] = await Promise.all([call('subscriptions/get', lastMessage), call('rooms/get', lastMessage)]); - - if (lastMessage) { - subscriptions = subscriptions.update; - rooms = rooms.update; - } - - const data = subscriptions.map((subscription) => { - const room = rooms.find(({ _id }) => _id === subscription.rid); - if (room) { - subscription.roomUpdatedAt = room._updatedAt; - subscription.lastMessage = normalizeMessage(room.lastMessage); - subscription.ro = room.ro; - subscription.description = room.description; - subscription.topic = room.topic; - subscription.announcement = room.announcement; - subscription.reactWhenReadOnly = room.reactWhenReadOnly; - subscription.archived = room.archived; - subscription.joinCodeRequired = room.joinCodeRequired; - } - if (subscription.roles) { - subscription.roles = subscription.roles.map(role => ({ value: role })); - } - return subscription; - }); - - - database.write(() => { - data.forEach(subscription => database.create('subscriptions', subscription, true)); - // rooms.forEach(room => database.create('rooms', room, true)); - }); - - - this.ddp.subscribe('stream-notify-user', `${ login.user.id }/subscriptions-changed`, false); - this.ddp.subscribe('stream-notify-user', `${ login.user.id }/rooms-changed`, false); - return data; - }, - disconnect() { - if (!this.ddp) { - return; - } - reduxStore.dispatch(disconnect_by_user()); - delete this.ddp; - return this.ddp.disconnect(); - }, - login(params, callback) { - return this.ddp.call('login', params).then((result) => { - if (typeof callback === 'function') { - callback(null, result); - } - return result; - }, (err) => { - if (/user not found/i.test(err.reason)) { - err.error = 1; - err.reason = 'User or Password incorrect'; - err.message = 'User or Password incorrect'; - } - if (typeof callback === 'function') { - callback(err, null); - } - return Promise.reject(err); - }); - }, - logout({ server }) { - if (this.ddp) { - this.ddp.logout(); - } - database.deleteAll(); - AsyncStorage.removeItem(TOKEN_KEY); - AsyncStorage.removeItem(`${ TOKEN_KEY }-${ server }`); - }, - async getSettings() { - const temp = database.objects('settings').sorted('_updatedAt', true)[0]; - const result = await (!temp ? call('public-settings/get') : call('public-settings/get', new Date(temp._updatedAt))); - const settings = temp ? result.update : result; - const filteredSettings = RocketChat._prepareSettings(RocketChat._filterSettings(settings)); - database.write(() => { - filteredSettings.forEach(setting => database.create('settings', setting, true)); - }); - reduxStore.dispatch(actions.addSettings(RocketChat.parseSettings(filteredSettings))); - }, + getSettings, + getPermissions, + getCustomEmoji, parseSettings: settings => settings.reduce((ret, item) => { ret[item._id] = item[settingsType[item.type]] || item.valueAsString || item.valueAsNumber || item.valueAsBoolean || item.value; @@ -755,16 +655,6 @@ const RocketChat = { }); }, _filterSettings: settings => settings.filter(setting => settingsType[setting.type] && setting.value), - async getPermissions() { - const temp = database.objects('permissions').sorted('_updatedAt', true)[0]; - const result = await (!temp ? call('permissions/get') : call('permissions/get', new Date(temp._updatedAt))); - let permissions = temp ? result.update : result; - permissions = RocketChat._preparePermissions(permissions); - database.write(() => { - permissions.forEach(permission => database.create('permissions', permission, true)); - }); - reduxStore.dispatch(actions.setAllPermissions(RocketChat.parsePermissions(permissions))); - }, parsePermissions: permissions => permissions.reduce((ret, item) => { ret[item._id] = item.roles.reduce((roleRet, role) => [...roleRet, role.value], []); return ret; @@ -775,16 +665,6 @@ const RocketChat = { }); return permissions; }, - async getCustomEmoji() { - const temp = database.objects('customEmojis').sorted('_updatedAt', true)[0]; - let emojis = await call('listEmojiCustom'); - emojis = emojis.filter(emoji => !temp || emoji._updatedAt > temp._updatedAt); - emojis = RocketChat._prepareEmojis(emojis); - database.write(() => { - emojis.forEach(emoji => database.create('customEmojis', emoji, true)); - }); - reduxStore.dispatch(actions.setCustomEmojis(RocketChat.parseEmojis(emojis))); - }, parseEmojis: emojis => emojis.reduce((ret, item) => { ret[item.name] = item.extension; item.aliases.forEach((alias) => { @@ -815,20 +695,21 @@ const RocketChat = { return call('pinMessage', message); }, getRoom(rid) { - const result = database.objects('subscriptions').filtered('rid = $0', rid); - if (result.length === 0) { + const [result] = database.objects('subscriptions').filtered('rid = $0', rid); + if (!result) { return Promise.reject(new Error('Room not found')); } - return Promise.resolve(result[0]); + return Promise.resolve(result); }, async getPermalink(message) { const room = await RocketChat.getRoom(message.rid); + const { server } = reduxStore.getState().server; const roomType = { p: 'group', c: 'channel', d: 'direct' }[room.t]; - return `${ room._server.id }/${ roomType }/${ room.name }?msg=${ message._id }`; + return `${ server }/${ roomType }/${ room.name }?msg=${ message._id }`; }, subscribe(...args) { return this.ddp.subscribe(...args); @@ -878,6 +759,12 @@ const RocketChat = { eraseRoom(rid) { return call('eraseRoom', rid); }, + toggleMuteUserInRoom(rid, username, mute) { + if (mute) { + return call('muteUserInRoom', { rid, username }); + } + return call('unmuteUserInRoom', { rid, username }); + }, toggleArchiveRoom(rid, archive) { if (archive) { return call('archiveRoom', rid); @@ -887,6 +774,17 @@ const RocketChat = { saveRoomSettings(rid, params) { return call('saveRoomSettings', rid, params); }, + saveNotificationSettings(rid, param, value) { + return call('saveNotificationSettings', rid, param, value); + }, + messageSearch(text, rid, limit) { + return call('messageSearch', text, rid, limit); + }, + addUsersToRoom(rid) { + let { users } = reduxStore.getState().selectedUsers; + users = users.map(u => u.name); + return call('addUsersToRoom', { rid, users }); + }, hasPermission(permissions, rid) { // get the room from realm const room = database.objects('subscriptions').filtered('rid = $0', rid)[0]; diff --git a/app/presentation/RoomItem.js b/app/presentation/RoomItem.js index 16677bb1d..5529dd15b 100644 --- a/app/presentation/RoomItem.js +++ b/app/presentation/RoomItem.js @@ -1,11 +1,13 @@ import React from 'react'; import moment from 'moment'; import PropTypes from 'prop-types'; -import { View, Text, StyleSheet } from 'react-native'; - +import { View, Text, StyleSheet, ViewPropTypes } from 'react-native'; +import Icon from 'react-native-vector-icons/MaterialIcons'; import { connect } from 'react-redux'; import SimpleMarkdown from 'simple-markdown'; +import messagesStatus from '../constants/messagesStatus'; + import Avatar from '../containers/Avatar'; import Status from '../containers/status'; import Touch from '../utils/touch/index'; //eslint-disable-line @@ -44,7 +46,6 @@ const styles = StyleSheet.create({ flex: 1, fontSize: 18, color: '#444', - marginRight: 8 }, lastMessage: { @@ -64,8 +65,8 @@ const styles = StyleSheet.create({ // backgroundColor: '#eee' }, row: { - width: '100%', - flex: 1, + // width: '100%', + // flex: 1, flexDirection: 'row', alignItems: 'flex-end', justifyContent: 'flex-end' @@ -145,41 +146,57 @@ const renderNumber = (unread, userMentions) => { ); }; +const attrs = ['name', 'unread', 'userMentions', 'alert', 'showLastMessage', 'type', '_updatedAt']; @connect(state => ({ user: state.login && state.login.user, - StoreLastMessage: state.settings.Store_Last_Message, - customEmojis: state.customEmojis + StoreLastMessage: state.settings.Store_Last_Message })) -export default class RoomItem extends React.PureComponent { +export default class RoomItem extends React.Component { static propTypes = { type: PropTypes.string.isRequired, name: PropTypes.string.isRequired, StoreLastMessage: PropTypes.bool, _updatedAt: PropTypes.instanceOf(Date), lastMessage: PropTypes.object, + showLastMessage: PropTypes.bool, favorite: PropTypes.bool, alert: PropTypes.bool, unread: PropTypes.number, userMentions: PropTypes.number, id: PropTypes.string, onPress: PropTypes.func, - customEmojis: PropTypes.object, - user: PropTypes.object + onLongPress: PropTypes.func, + user: PropTypes.object, + avatarSize: PropTypes.number, + statusStyle: ViewPropTypes.style } + static defaultProps = { + showLastMessage: true, + avatarSize: 46 + } + shouldComponentUpdate(nextProps) { + const oldlastMessage = this.props.lastMessage; + const newLastmessage = nextProps.lastMessage; + + if (oldlastMessage && newLastmessage && oldlastMessage.ts !== newLastmessage.ts) { + return true; + } + return attrs.some(key => nextProps[key] !== this.props[key]); + } get icon() { const { - type, name, id + type, name, id, avatarSize, statusStyle } = this.props; - return ({type === 'd' ? : null }); + return ({type === 'd' ? : null }); } get lastMessage() { const { - lastMessage, type + lastMessage, type, showLastMessage } = this.props; - if (!this.props.StoreLastMessage) { + if (!this.props.StoreLastMessage || !showLastMessage) { return ''; } if (!lastMessage) { @@ -208,7 +225,7 @@ export default class RoomItem extends React.PureComponent { render() { const { - favorite, unread, userMentions, name, _updatedAt, customEmojis, alert + favorite, unread, userMentions, name, _updatedAt, alert, status } = this.props; const date = this.formatDate(_updatedAt); @@ -224,10 +241,19 @@ export default class RoomItem extends React.PureComponent { accessibilityLabel += ', you were mentioned'; } - accessibilityLabel += `, last message ${ date }`; + if (date) { + accessibilityLabel += `, last message ${ date }`; + } return ( - + {this.icon} @@ -236,9 +262,9 @@ export default class RoomItem extends React.PureComponent { {_updatedAt ? { date } : null} + {status === messagesStatus.ERROR ? : null } item.name !== action.user.name) - }; - case CREATE_CHANNEL.RESET: - return initialState; default: return state; } diff --git a/app/reducers/index.js b/app/reducers/index.js index 3a4c0dac1..6f66738e6 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -7,6 +7,7 @@ import room from './room'; import rooms from './rooms'; import server from './server'; import navigator from './navigator'; +import selectedUsers from './selectedUsers'; import createChannel from './createChannel'; import app from './app'; import permissions from './permissions'; @@ -26,6 +27,7 @@ export default combineReducers({ messages, server, navigator, + selectedUsers, createChannel, app, room, diff --git a/app/reducers/mentionedMessages.js b/app/reducers/mentionedMessages.js index ee5883fba..f8c445dd9 100644 --- a/app/reducers/mentionedMessages.js +++ b/app/reducers/mentionedMessages.js @@ -1,11 +1,22 @@ import { MENTIONED_MESSAGES } from '../actions/actionsTypes'; const initialState = { - messages: [] + messages: [], + ready: false }; export default function server(state = initialState, action) { switch (action.type) { + case MENTIONED_MESSAGES.OPEN: + return { + ...state, + ready: false + }; + case MENTIONED_MESSAGES.READY: + return { + ...state, + ready: true + }; case MENTIONED_MESSAGES.MESSAGES_RECEIVED: return { ...state, diff --git a/app/reducers/pinnedMessages.js b/app/reducers/pinnedMessages.js index 85cc62683..d912e5b7a 100644 --- a/app/reducers/pinnedMessages.js +++ b/app/reducers/pinnedMessages.js @@ -2,7 +2,8 @@ import { PINNED_MESSAGES } from '../actions/actionsTypes'; const initialState = { messages: [], - isOpen: false + isOpen: false, + ready: false }; export default function server(state = initialState, action) { @@ -10,7 +11,13 @@ export default function server(state = initialState, action) { case PINNED_MESSAGES.OPEN: return { ...state, - isOpen: true + isOpen: true, + ready: false + }; + case PINNED_MESSAGES.READY: + return { + ...state, + ready: true }; case PINNED_MESSAGES.MESSAGES_RECEIVED: return { diff --git a/app/reducers/roomFiles.js b/app/reducers/roomFiles.js index c27382d4d..911f295a7 100644 --- a/app/reducers/roomFiles.js +++ b/app/reducers/roomFiles.js @@ -1,11 +1,22 @@ import { ROOM_FILES } from '../actions/actionsTypes'; const initialState = { - messages: [] + messages: [], + ready: false }; export default function server(state = initialState, action) { switch (action.type) { + case ROOM_FILES.OPEN: + return { + ...state, + ready: false + }; + case ROOM_FILES.READY: + return { + ...state, + ready: true + }; case ROOM_FILES.MESSAGES_RECEIVED: return { ...state, diff --git a/app/reducers/selectedUsers.js b/app/reducers/selectedUsers.js new file mode 100644 index 000000000..5f455aea4 --- /dev/null +++ b/app/reducers/selectedUsers.js @@ -0,0 +1,30 @@ +import { SELECTED_USERS } from '../actions/actionsTypes'; + +const initialState = { + users: [], + loading: false +}; + +export default function messages(state = initialState, action) { + switch (action.type) { + case SELECTED_USERS.ADD_USER: + return { + ...state, + users: state.users.concat(action.user) + }; + case SELECTED_USERS.REMOVE_USER: + return { + ...state, + users: state.users.filter(item => item.name !== action.user.name) + }; + case SELECTED_USERS.SET_LOADING: + return { + ...state, + loading: action.loading + }; + case SELECTED_USERS.RESET: + return initialState; + default: + return state; + } +} diff --git a/app/reducers/server.js b/app/reducers/server.js index 337c393e8..c68e95dbc 100644 --- a/app/reducers/server.js +++ b/app/reducers/server.js @@ -5,7 +5,8 @@ const initialState = { connected: false, errorMessage: '', failure: false, - server: '' + server: '', + adding: false }; @@ -32,8 +33,17 @@ export default function server(state = initialState, action) { failure: true, errorMessage: action.err }; + case SERVER.ADD: + return { + ...state, + adding: true + }; case SERVER.SELECT: - return { ...state, server: action.server }; + return { + ...state, + server: action.server, + adding: false + }; default: return state; } diff --git a/app/reducers/snippetedMessages.js b/app/reducers/snippetedMessages.js index 839ff7d1b..727ed41ae 100644 --- a/app/reducers/snippetedMessages.js +++ b/app/reducers/snippetedMessages.js @@ -1,11 +1,22 @@ import { SNIPPETED_MESSAGES } from '../actions/actionsTypes'; const initialState = { - messages: [] + messages: [], + ready: false }; export default function server(state = initialState, action) { switch (action.type) { + case SNIPPETED_MESSAGES.OPEN: + return { + ...state, + ready: false + }; + case SNIPPETED_MESSAGES.READY: + return { + ...state, + ready: true + }; case SNIPPETED_MESSAGES.MESSAGES_RECEIVED: return { ...state, diff --git a/app/reducers/starredMessages.js b/app/reducers/starredMessages.js index 704fbcfab..b96fbe87c 100644 --- a/app/reducers/starredMessages.js +++ b/app/reducers/starredMessages.js @@ -2,7 +2,8 @@ import { STARRED_MESSAGES } from '../actions/actionsTypes'; const initialState = { messages: [], - isOpen: false + isOpen: false, + ready: false }; export default function server(state = initialState, action) { @@ -10,7 +11,13 @@ export default function server(state = initialState, action) { case STARRED_MESSAGES.OPEN: return { ...state, - isOpen: true + isOpen: true, + ready: false + }; + case STARRED_MESSAGES.READY: + return { + ...state, + ready: true }; case STARRED_MESSAGES.MESSAGES_RECEIVED: return { diff --git a/app/sagas/connect.js b/app/sagas/connect.js index 8291073c7..7c494275d 100644 --- a/app/sagas/connect.js +++ b/app/sagas/connect.js @@ -1,44 +1,44 @@ -import { call, takeLatest, select, take, race } from 'redux-saga/effects'; -import { delay } from 'redux-saga'; +import { call, takeLatest, select, put, all } from 'redux-saga/effects'; +import { AsyncStorage } from 'react-native'; import { METEOR } from '../actions/actionsTypes'; import RocketChat from '../lib/rocketchat'; +import { setToken } from '../actions/login'; const getServer = ({ server }) => server.server; - - -const connect = url => RocketChat.connect(url); -const watchConnect = function* watchConnect() { - const { disconnect } = yield race({ - disconnect: take(METEOR.DISCONNECT), - disconnected_by_user: take(METEOR.DISCONNECT_BY_USER) - }); - if (disconnect) { - while (true) { - const { connected } = yield race({ - connected: take(METEOR.SUCCESS), - timeout: call(delay, 1000) - }); - if (connected) { - return; - } - yield RocketChat.reconnect(); +const getToken = function* getToken() { + const currentServer = yield select(getServer); + const user = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`); + if (user) { + yield put(setToken(JSON.parse(user))); + try { + yield call([AsyncStorage, 'setItem'], RocketChat.TOKEN_KEY, JSON.parse(user).token || ''); + } catch (error) { + console.warn('getToken', error); } + return JSON.parse(user); } + return yield put(setToken()); }; + + +const connect = (...args) => RocketChat.connect(...args); + const test = function* test() { - // try { - const server = yield select(getServer); - // const response = - yield call(connect, server); + try { + const server = yield select(getServer); + const user = yield call(getToken); + // const response = + yield all([call(connect, server, user && user.token ? { resume: user.token, ...user.user } : undefined)]);// , put(loginRequest({ resume: user.token }))]); // yield put(connectSuccess(response)); - // } catch (err) { + } catch (err) { + console.warn('test', err); // yield put(connectFailure(err.status)); - // } + } }; const root = function* root() { yield takeLatest(METEOR.REQUEST, test); // yield take(METEOR.SUCCESS, watchConnect); - yield takeLatest(METEOR.SUCCESS, watchConnect); + // yield takeLatest(METEOR.SUCCESS, watchConnect); }; export default root; diff --git a/app/sagas/createChannel.js b/app/sagas/createChannel.js index f4161201d..b538c3fb9 100644 --- a/app/sagas/createChannel.js +++ b/app/sagas/createChannel.js @@ -28,4 +28,5 @@ const handleRequest = function* handleRequest({ data }) { const root = function* root() { yield takeLatest(CREATE_CHANNEL.REQUEST, handleRequest); }; + export default root; diff --git a/app/sagas/hello.js b/app/sagas/hello.js deleted file mode 100644 index 83a72731e..000000000 --- a/app/sagas/hello.js +++ /dev/null @@ -1,14 +0,0 @@ -import { take, fork } from 'redux-saga/effects'; - -const foreverAlone = function* foreverAlone() { - yield take('FOI'); - console.log('FOIIIIIII'); - yield take('voa'); - console.log('o'); -}; - -const root = function* root() { - yield fork(foreverAlone); -}; - -export default root; diff --git a/app/sagas/index.js b/app/sagas/index.js index ab82fdefc..6489d7a32 100644 --- a/app/sagas/index.js +++ b/app/sagas/index.js @@ -1,5 +1,4 @@ import { all } from 'redux-saga/effects'; -import hello from './hello'; import login from './login'; import connect from './connect'; import rooms from './rooms'; @@ -18,7 +17,6 @@ const root = function* root() { yield all([ init(), createChannel(), - hello(), rooms(), login(), connect(), diff --git a/app/sagas/init.js b/app/sagas/init.js index 423e91a96..0360d82b3 100644 --- a/app/sagas/init.js +++ b/app/sagas/init.js @@ -2,15 +2,13 @@ import { AsyncStorage } from 'react-native'; import { call, put, takeLatest } from 'redux-saga/effects'; import * as actions from '../actions'; import { setServer } from '../actions/server'; -import { restoreToken } from '../actions/login'; +import { restoreToken, setUser } from '../actions/login'; import { APP } from '../actions/actionsTypes'; -import { setRoles } from '../actions/roles'; -import database from '../lib/realm'; import RocketChat from '../lib/rocketchat'; const restore = function* restore() { try { - const token = yield call([AsyncStorage, 'getItem'], 'reactnativemeteor_usertoken'); + const token = yield call([AsyncStorage, 'getItem'], RocketChat.TOKEN_KEY); if (token) { yield put(restoreToken(token)); } @@ -18,21 +16,16 @@ const restore = function* restore() { const currentServer = yield call([AsyncStorage, 'getItem'], 'currentServer'); if (currentServer) { yield put(setServer(currentServer)); - const settings = database.objects('settings'); - yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length)))); - const permissions = database.objects('permissions'); - yield put(actions.setAllPermissions(RocketChat.parsePermissions(permissions.slice(0, permissions.length)))); - const emojis = database.objects('customEmojis'); - yield put(actions.setCustomEmojis(RocketChat.parseEmojis(emojis.slice(0, emojis.length)))); - const roles = database.objects('roles'); - yield put(setRoles(roles.reduce((result, role) => { - result[role._id] = role.description; - return result; - }, {}))); + + const login = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`); + if (login && login.user) { + yield put(setUser(login.user)); + } } + yield put(actions.appReady({})); } catch (e) { - console.log(e); + console.warn('restore', e); } }; diff --git a/app/sagas/login.js b/app/sagas/login.js index f78febf50..41de7368d 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -1,16 +1,16 @@ import { AsyncStorage } from 'react-native'; -import { put, call, takeLatest, select, all, take } from 'redux-saga/effects'; -import { Answers } from 'react-native-fabric'; +import { put, call, take, takeLatest, select, all } from 'redux-saga/effects'; + import * as types from '../actions/actionsTypes'; import { - loginRequest, - loginSubmit, + // loginRequest, + // loginSubmit, registerRequest, registerIncomplete, - loginSuccess, + // loginSuccess, loginFailure, - logout, - setToken, + // logout, + // setToken, registerSuccess, setUsernameRequest, setUsernameSuccess, @@ -23,40 +23,41 @@ import * as NavigationService from '../containers/routes/NavigationService'; const getUser = state => state.login; const getServer = state => state.server.server; const getIsConnected = state => state.meteor.connected; -const loginCall = args => ((args.resume || args.oauth) ? RocketChat.login(args) : RocketChat.loginWithPassword(args)); + +// const loginCall = args => ((args.resume || args.oauth) ? RocketChat.login(args) : RocketChat.loginWithPassword(args)); +const loginCall = args => RocketChat.loginWithPassword(args); const registerCall = args => RocketChat.register(args); const setUsernameCall = args => RocketChat.setUsername(args); +const loginSuccessCall = () => RocketChat.loginSuccess(); const logoutCall = args => RocketChat.logout(args); -const meCall = args => RocketChat.me(args); const forgotPasswordCall = args => RocketChat.forgotPassword(args); -const userInfoCall = args => RocketChat.userInfo(args); -const getToken = function* getToken() { - const currentServer = yield select(getServer); - const user = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`); - if (user) { - try { - yield put(setToken(JSON.parse(user))); - yield call([AsyncStorage, 'setItem'], RocketChat.TOKEN_KEY, JSON.parse(user).token || ''); - return JSON.parse(user); - } catch (e) { - console.log('getTokenerr', e); - } - } else { - return yield put(setToken()); - } -}; +// const getToken = function* getToken() { +// const currentServer = yield select(getServer); +// const user = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`); +// if (user) { +// try { +// yield put(setToken(JSON.parse(user))); +// yield call([AsyncStorage, 'setItem'], RocketChat.TOKEN_KEY, JSON.parse(user).token || ''); +// return JSON.parse(user); +// } catch (e) { +// console.log('getTokenerr', e); +// } +// } else { +// return yield put(setToken()); +// } +// }; -const handleLoginWhenServerChanges = function* handleLoginWhenServerChanges() { - try { - const user = yield call(getToken); - if (user.token) { - yield put(loginRequest({ resume: user.token })); - } - } catch (e) { - console.log(e); - } -}; +// const handleLoginWhenServerChanges = function* handleLoginWhenServerChanges() { +// try { +// const user = yield call(getToken); +// if (user.token) { +// yield put(loginRequest({ resume: user.token })); +// } +// } catch (e) { +// console.log(e); +// } +// }; const saveToken = function* saveToken() { const [server, user] = yield all([select(getServer), select(getUser)]); @@ -64,41 +65,29 @@ const saveToken = function* saveToken() { yield AsyncStorage.setItem(`${ RocketChat.TOKEN_KEY }-${ server }`, JSON.stringify(user)); const token = yield AsyncStorage.getItem('pushId'); if (token) { - RocketChat.registerPushToken(user.user.id, token); + yield RocketChat.registerPushToken(user.user.id, token); } - Answers.logLogin('Email', true, { server }); -}; - -const handleLoginRequest = function* handleLoginRequest({ credentials }) { - try { - const server = yield select(getServer); - const user = yield call(loginCall, credentials); - - // GET /me from REST API - const me = yield call(meCall, { server, token: user.token, userId: user.id }); - - // if user has username - if (me.username) { - const userInfo = yield call(userInfoCall, { server, token: user.token, userId: user.id }); - user.username = userInfo.user.username; - if (userInfo.user.roles) { - user.roles = userInfo.user.roles; - } - } else { - yield put(registerIncomplete()); - } - yield put(loginSuccess(user)); - } catch (err) { - if (err.error === 403) { - return yield put(logout()); - } - yield put(loginFailure(err)); + if (!user.user.username && !user.isRegistering) { + yield put(registerIncomplete()); } }; -const handleLoginSubmit = function* handleLoginSubmit({ credentials }) { - yield put(loginRequest(credentials)); -}; +// const handleLoginRequest = function* handleLoginRequest({ credentials }) { +// try { +// // const server = yield select(getServer); +// const user = yield call(loginCall, credentials); +// yield put(loginSuccess(user)); +// } catch (err) { +// if (err.error === 403) { +// return yield put(logout()); +// } +// yield put(loginFailure(err)); +// } +// }; + +// const handleLoginSubmit = function* handleLoginSubmit({ credentials }) { +// yield put(loginRequest(credentials)); +// }; const handleRegisterSubmit = function* handleRegisterSubmit({ credentials }) { yield put(registerRequest(credentials)); @@ -114,10 +103,14 @@ const handleRegisterRequest = function* handleRegisterRequest({ credentials }) { }; const handleRegisterSuccess = function* handleRegisterSuccess({ credentials }) { - yield put(loginSubmit({ - username: credentials.email, - password: credentials.pass - })); + try { + yield call(loginCall, { + username: credentials.email, + password: credentials.pass + }); + } catch (err) { + yield put(loginFailure(err)); + } }; const handleSetUsernameSubmit = function* handleSetUsernameSubmit({ credentials }) { @@ -128,6 +121,7 @@ const handleSetUsernameRequest = function* handleSetUsernameRequest({ credential try { yield call(setUsernameCall, { credentials }); yield put(setUsernameSuccess()); + yield call(loginSuccessCall); } catch (err) { yield put(loginFailure(err)); } @@ -154,20 +148,24 @@ const handleForgotPasswordRequest = function* handleForgotPasswordRequest({ emai }; const watchLoginOpen = function* watchLoginOpen() { - const isConnected = yield select(getIsConnected); - if (!isConnected) { - yield take(types.METEOR.SUCCESS); + try { + const isConnected = yield select(getIsConnected); + if (!isConnected) { + yield take(types.METEOR.SUCCESS); + } + const sub = yield RocketChat.subscribe('meteor.loginServiceConfiguration'); + yield take(types.LOGIN.CLOSE); + sub.unsubscribe().catch(e => console.warn('watchLoginOpen unsubscribe', e)); + } catch (error) { + console.warn('watchLoginOpen', error); } - const sub = yield RocketChat.subscribe('meteor.loginServiceConfiguration'); - yield take(types.LOGIN.CLOSE); - sub.unsubscribe().catch(e => alert(e)); }; const root = function* root() { - yield takeLatest(types.METEOR.SUCCESS, handleLoginWhenServerChanges); - yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest); + // yield takeLatest(types.METEOR.SUCCESS, handleLoginWhenServerChanges); + // yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest); yield takeLatest(types.LOGIN.SUCCESS, saveToken); - yield takeLatest(types.LOGIN.SUBMIT, handleLoginSubmit); + // yield takeLatest(types.LOGIN.SUBMIT, handleLoginSubmit); yield takeLatest(types.LOGIN.REGISTER_REQUEST, handleRegisterRequest); yield takeLatest(types.LOGIN.REGISTER_SUBMIT, handleRegisterSubmit); yield takeLatest(types.LOGIN.REGISTER_SUCCESS, handleRegisterSuccess); diff --git a/app/sagas/mentionedMessages.js b/app/sagas/mentionedMessages.js index e367a2ae9..0a1d0b464 100644 --- a/app/sagas/mentionedMessages.js +++ b/app/sagas/mentionedMessages.js @@ -1,14 +1,31 @@ -import { take, takeLatest } from 'redux-saga/effects'; +import { put, takeLatest } from 'redux-saga/effects'; import * as types from '../actions/actionsTypes'; import RocketChat from '../lib/rocketchat'; +import { readyMentionedMessages } from '../actions/mentionedMessages'; -const watchMentionedMessagesRoom = function* watchMentionedMessagesRoom({ rid }) { - const sub = yield RocketChat.subscribe('mentionedMessages', rid, 50); - yield take(types.MENTIONED_MESSAGES.CLOSE); - sub.unsubscribe().catch(e => alert(e)); +let sub; +let newSub; + +const openMentionedMessagesRoom = function* openMentionedMessagesRoom({ rid, limit }) { + newSub = yield RocketChat.subscribe('mentionedMessages', rid, limit); + yield put(readyMentionedMessages()); + if (sub) { + sub.unsubscribe().catch(e => console.warn('openMentionedMessagesRoom', e)); + } + sub = newSub; +}; + +const closeMentionedMessagesRoom = function* closeMentionedMessagesRoom() { + if (sub) { + yield sub.unsubscribe().catch(e => console.warn('closeMentionedMessagesRoom sub', e)); + } + if (newSub) { + yield newSub.unsubscribe().catch(e => console.warn('closeMentionedMessagesRoom newSub', e)); + } }; const root = function* root() { - yield takeLatest(types.MENTIONED_MESSAGES.OPEN, watchMentionedMessagesRoom); + yield takeLatest(types.MENTIONED_MESSAGES.OPEN, openMentionedMessagesRoom); + yield takeLatest(types.MENTIONED_MESSAGES.CLOSE, closeMentionedMessagesRoom); }; export default root; diff --git a/app/sagas/messages.js b/app/sagas/messages.js index 4f806aaec..7db85f934 100644 --- a/app/sagas/messages.js +++ b/app/sagas/messages.js @@ -1,5 +1,5 @@ -import { takeLatest, select, take, put, call } from 'redux-saga/effects'; -import { MESSAGES, LOGIN } from '../actions/actionsTypes'; +import { takeLatest, put, call } from 'redux-saga/effects'; +import { MESSAGES } from '../actions/actionsTypes'; import { messagesSuccess, messagesFailure, @@ -22,16 +22,16 @@ const toggleStarMessage = message => RocketChat.toggleStarMessage(message); const getPermalink = message => RocketChat.getPermalink(message); const togglePinMessage = message => RocketChat.togglePinMessage(message); -const get = function* get({ rid }) { - const auth = yield select(state => state.login.isAuthenticated); - if (!auth) { - yield take(LOGIN.SUCCESS); - } +const get = function* get({ room }) { try { - yield RocketChat.loadMessagesForRoom(rid, null); + if (room.lastOpen) { + yield RocketChat.loadMissedMessages(room); + } else { + yield RocketChat.loadMessagesForRoom(room); + } yield put(messagesSuccess()); } catch (err) { - console.log(err); + console.warn('messagesFailure', err); yield put(messagesFailure(err.status)); } }; diff --git a/app/sagas/pinnedMessages.js b/app/sagas/pinnedMessages.js index 95020a1a3..a451e322e 100644 --- a/app/sagas/pinnedMessages.js +++ b/app/sagas/pinnedMessages.js @@ -1,14 +1,31 @@ -import { take, takeLatest } from 'redux-saga/effects'; +import { put, takeLatest } from 'redux-saga/effects'; import * as types from '../actions/actionsTypes'; import RocketChat from '../lib/rocketchat'; +import { readyPinnedMessages } from '../actions/pinnedMessages'; -const watchPinnedMessagesRoom = function* watchPinnedMessagesRoom({ rid }) { - const sub = yield RocketChat.subscribe('pinnedMessages', rid, 50); - yield take(types.PINNED_MESSAGES.CLOSE); - sub.unsubscribe().catch(e => alert(e)); +let sub; +let newSub; + +const openPinnedMessagesRoom = function* openPinnedMessagesRoom({ rid, limit }) { + newSub = yield RocketChat.subscribe('pinnedMessages', rid, limit); + yield put(readyPinnedMessages()); + if (sub) { + sub.unsubscribe().catch(e => console.warn('openPinnedMessagesRoom', e)); + } + sub = newSub; +}; + +const closePinnedMessagesRoom = function* closePinnedMessagesRoom() { + if (sub) { + yield sub.unsubscribe().catch(e => console.warn('closePinnedMessagesRoom sub', e)); + } + if (newSub) { + yield newSub.unsubscribe().catch(e => console.warn('closePinnedMessagesRoom newSub', e)); + } }; const root = function* root() { - yield takeLatest(types.PINNED_MESSAGES.OPEN, watchPinnedMessagesRoom); + yield takeLatest(types.PINNED_MESSAGES.OPEN, openPinnedMessagesRoom); + yield takeLatest(types.PINNED_MESSAGES.CLOSE, closePinnedMessagesRoom); }; export default root; diff --git a/app/sagas/roomFiles.js b/app/sagas/roomFiles.js index a2be1c530..28e919dc4 100644 --- a/app/sagas/roomFiles.js +++ b/app/sagas/roomFiles.js @@ -1,14 +1,31 @@ -import { take, takeLatest } from 'redux-saga/effects'; +import { put, takeLatest } from 'redux-saga/effects'; import * as types from '../actions/actionsTypes'; import RocketChat from '../lib/rocketchat'; +import { readyRoomFiles } from '../actions/roomFiles'; -const watchRoomFiles = function* watchRoomFiles({ rid }) { - const sub = yield RocketChat.subscribe('roomFiles', rid, 50); - yield take(types.ROOM_FILES.CLOSE); - sub.unsubscribe().catch(e => alert(e)); +let sub; +let newSub; + +const openRoomFiles = function* openRoomFiles({ rid, limit }) { + newSub = yield RocketChat.subscribe('roomFiles', rid, limit); + yield put(readyRoomFiles()); + if (sub) { + sub.unsubscribe().catch(e => console.warn('openRoomFiles', e)); + } + sub = newSub; +}; + +const closeRoomFiles = function* closeRoomFiles() { + if (sub) { + yield sub.unsubscribe().catch(e => console.warn('closeRoomFiles sub', e)); + } + if (newSub) { + yield newSub.unsubscribe().catch(e => console.warn('closeRoomFiles newSub', e)); + } }; const root = function* root() { - yield takeLatest(types.ROOM_FILES.OPEN, watchRoomFiles); + yield takeLatest(types.ROOM_FILES.OPEN, openRoomFiles); + yield takeLatest(types.ROOM_FILES.CLOSE, closeRoomFiles); }; export default root; diff --git a/app/sagas/rooms.js b/app/sagas/rooms.js index ded0d4e63..63c1b335c 100644 --- a/app/sagas/rooms.js +++ b/app/sagas/rooms.js @@ -1,9 +1,9 @@ import { Alert } from 'react-native'; import { put, call, takeLatest, take, select, race, fork, cancel, takeEvery } from 'redux-saga/effects'; import { delay } from 'redux-saga'; -import { FOREGROUND, BACKGROUND } from 'redux-enhancer-react-native-appstate'; +import { BACKGROUND } from 'redux-enhancer-react-native-appstate'; import * as types from '../actions/actionsTypes'; -import { roomsSuccess, roomsFailure } from '../actions/rooms'; +// import { roomsSuccess, roomsFailure } from '../actions/rooms'; import { addUserTyping, removeUserTyping, setLastOpen } from '../actions/room'; import { messagesRequest } from '../actions/messages'; import RocketChat from '../lib/rocketchat'; @@ -13,18 +13,18 @@ import * as NavigationService from '../containers/routes/NavigationService'; const leaveRoom = rid => RocketChat.leaveRoom(rid); const eraseRoom = rid => RocketChat.eraseRoom(rid); -const getRooms = function* getRooms() { - return yield RocketChat.getRooms(); -}; +// const getRooms = function* getRooms() { +// return yield RocketChat.getRooms(); +// }; -const watchRoomsRequest = function* watchRoomsRequest() { - try { - yield call(getRooms); - yield put(roomsSuccess()); - } catch (err) { - yield put(roomsFailure(err.status)); - } -}; +// const watchRoomsRequest = function* watchRoomsRequest() { +// try { +// yield call(getRooms); +// yield put(roomsSuccess()); +// } catch (err) { +// yield put(roomsFailure(err.status)); +// } +// }; const cancelTyping = function* cancelTyping(username) { while (true) { @@ -50,45 +50,46 @@ const usersTyping = function* usersTyping({ rid }) { } }; const handleMessageReceived = function* handleMessageReceived({ message }) { - const room = yield select(state => state.room); + try { + const room = yield select(state => state.room); - if (message.rid === room.rid) { - database.write(() => { - database.create('messages', message, true); - }); + if (message.rid === room.rid) { + database.write(() => { + database.create('messages', message, true); + }); - RocketChat.readMessages(room.rid); + RocketChat.readMessages(room.rid); + } + } catch (e) { + console.warn('handleMessageReceived', e); } }; const watchRoomOpen = function* watchRoomOpen({ room }) { - const auth = yield select(state => state.login.isAuthenticated); - if (!auth) { - yield take(types.LOGIN.SUCCESS); - } + yield put(messagesRequest({ ...room })); + // const { open } = yield race({ + // messages: take(types.MESSAGES.SUCCESS), + // open: take(types.ROOM.OPEN) + // }); + // + // if (open) { + // return; + // } - - yield put(messagesRequest({ rid: room.rid })); - - const { open } = yield race({ - messages: take(types.MESSAGES.SUCCESS), - open: take(types.ROOM.OPEN) - }); - - if (open) { - return; - } RocketChat.readMessages(room.rid); - const subscriptions = yield Promise.all([RocketChat.subscribe('stream-room-messages', room.rid, false), RocketChat.subscribe('stream-notify-room', `${ room.rid }/typing`, false)]); + const sub = yield RocketChat.subscribeRoom(room); + // const subscriptions = yield Promise.all([RocketChat.subscribe('stream-room-messages', room.rid, false), RocketChat.subscribe('stream-notify-room', `${ room.rid }/typing`, false)]); const thread = yield fork(usersTyping, { rid: room.rid }); yield race({ open: take(types.ROOM.OPEN), close: take(types.ROOM.CLOSE) }); cancel(thread); - subscriptions.forEach((sub) => { - sub.unsubscribe().catch(e => alert(e)); - }); + sub.stop(); + + // subscriptions.forEach((sub) => { + // sub.unsubscribe().catch(e => alert(e)); + // }); }; const watchuserTyping = function* watchuserTyping({ status }) { @@ -110,13 +111,13 @@ const watchuserTyping = function* watchuserTyping({ status }) { } }; -const updateRoom = function* updateRoom() { - const room = yield select(state => state.room); - if (!room || !room.rid) { - return; - } - yield put(messagesRequest({ rid: room.rid })); -}; +// const updateRoom = function* updateRoom() { +// const room = yield select(state => state.room); +// if (!room || !room.rid) { +// return; +// } +// yield put(messagesRequest({ rid: room.rid })); +// }; const updateLastOpen = function* updateLastOpen() { yield put(setLastOpen()); @@ -157,11 +158,10 @@ const handleEraseRoom = function* handleEraseRoom({ rid }) { const root = function* root() { yield takeLatest(types.ROOM.USER_TYPING, watchuserTyping); - yield takeLatest(types.LOGIN.SUCCESS, watchRoomsRequest); yield takeLatest(types.ROOM.OPEN, watchRoomOpen); yield takeEvery(types.ROOM.MESSAGE_RECEIVED, handleMessageReceived); - yield takeLatest(FOREGROUND, updateRoom); - yield takeLatest(FOREGROUND, watchRoomsRequest); + // yield takeLatest(FOREGROUND, updateRoom); + // yield takeLatest(FOREGROUND, watchRoomsRequest); yield takeLatest(BACKGROUND, updateLastOpen); yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom); yield takeLatest(types.ROOM.ERASE, handleEraseRoom); diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js index ff67573a4..6aaf7ae3f 100644 --- a/app/sagas/selectServer.js +++ b/app/sagas/selectServer.js @@ -3,8 +3,9 @@ import { delay } from 'redux-saga'; import { AsyncStorage } from 'react-native'; import { SERVER } from '../actions/actionsTypes'; import * as actions from '../actions'; -import { connectRequest, disconnect, disconnect_by_user } from '../actions/connect'; -import { changedServer, serverSuccess, serverFailure, serverRequest, setServer } from '../actions/server'; +import { connectRequest } from '../actions/connect'; +import { serverSuccess, serverFailure, serverRequest, setServer } from '../actions/server'; +import { setRoles } from '../actions/roles'; import RocketChat from '../lib/rocketchat'; import database from '../lib/realm'; import * as NavigationService from '../containers/routes/NavigationService'; @@ -14,16 +15,28 @@ const validate = function* validate(server) { }; const selectServer = function* selectServer({ server }) { - yield database.setActiveDB(server); - yield put(disconnect_by_user()); - yield put(disconnect()); - yield put(changedServer(server)); - yield call([AsyncStorage, 'setItem'], 'currentServer', server); - const settings = database.objects('settings'); - yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length)))); - const permissions = database.objects('permissions'); - yield put(actions.setAllPermissions(RocketChat.parsePermissions(permissions.slice(0, permissions.length)))); - yield put(connectRequest(server)); + try { + yield database.setActiveDB(server); + + // yield RocketChat.disconnect(); + + yield call([AsyncStorage, 'setItem'], 'currentServer', server); + const settings = database.objects('settings'); + yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length)))); + const permissions = database.objects('permissions'); + yield put(actions.setAllPermissions(RocketChat.parsePermissions(permissions.slice(0, permissions.length)))); + const emojis = database.objects('customEmojis'); + yield put(actions.setCustomEmojis(RocketChat.parseEmojis(emojis.slice(0, emojis.length)))); + const roles = database.objects('roles'); + yield put(setRoles(roles.reduce((result, role) => { + result[role._id] = role.description; + return result; + }, {}))); + + yield put(connectRequest(server)); + } catch (e) { + console.warn('selectServer', e); + } }; const validateServer = function* validateServer({ server }) { @@ -32,7 +45,7 @@ const validateServer = function* validateServer({ server }) { yield call(validate, server); yield put(serverSuccess()); } catch (e) { - console.log(e); + console.warn('validateServer', e); yield put(serverFailure(e)); } }; diff --git a/app/sagas/snippetedMessages.js b/app/sagas/snippetedMessages.js index 081c0f90f..120cb12f9 100644 --- a/app/sagas/snippetedMessages.js +++ b/app/sagas/snippetedMessages.js @@ -1,14 +1,31 @@ -import { take, takeLatest } from 'redux-saga/effects'; +import { put, takeLatest } from 'redux-saga/effects'; import * as types from '../actions/actionsTypes'; import RocketChat from '../lib/rocketchat'; +import { readySnippetedMessages } from '../actions/snippetedMessages'; -const watchSnippetedMessagesRoom = function* watchSnippetedMessagesRoom({ rid }) { - const sub = yield RocketChat.subscribe('snippetedMessages', rid, 50); - yield take(types.SNIPPETED_MESSAGES.CLOSE); - sub.unsubscribe().catch(e => alert(e)); +let sub; +let newSub; + +const openSnippetedMessagesRoom = function* openSnippetedMessagesRoom({ rid, limit }) { + newSub = yield RocketChat.subscribe('snippetedMessages', rid, limit); + yield put(readySnippetedMessages()); + if (sub) { + sub.unsubscribe().catch(e => console.warn('openSnippetedMessagesRoom', e)); + } + sub = newSub; +}; + +const closeSnippetedMessagesRoom = function* closeSnippetedMessagesRoom() { + if (sub) { + yield sub.unsubscribe().catch(e => console.warn('closeSnippetedMessagesRoom sub', e)); + } + if (newSub) { + yield newSub.unsubscribe().catch(e => console.warn('closeSnippetedMessagesRoom newSub', e)); + } }; const root = function* root() { - yield takeLatest(types.SNIPPETED_MESSAGES.OPEN, watchSnippetedMessagesRoom); + yield takeLatest(types.SNIPPETED_MESSAGES.OPEN, openSnippetedMessagesRoom); + yield takeLatest(types.SNIPPETED_MESSAGES.CLOSE, closeSnippetedMessagesRoom); }; export default root; diff --git a/app/sagas/starredMessages.js b/app/sagas/starredMessages.js index 4d07d65ed..2ee8d2d1b 100644 --- a/app/sagas/starredMessages.js +++ b/app/sagas/starredMessages.js @@ -1,14 +1,31 @@ -import { take, takeLatest } from 'redux-saga/effects'; +import { put, takeLatest } from 'redux-saga/effects'; import * as types from '../actions/actionsTypes'; import RocketChat from '../lib/rocketchat'; +import { readyStarredMessages } from '../actions/starredMessages'; -const watchStarredMessagesRoom = function* watchStarredMessagesRoom({ rid }) { - const sub = yield RocketChat.subscribe('starredMessages', rid, 50); - yield take(types.STARRED_MESSAGES.CLOSE); - sub.unsubscribe().catch(e => alert(e)); +let sub; +let newSub; + +const openStarredMessagesRoom = function* openStarredMessagesRoom({ rid, limit }) { + newSub = yield RocketChat.subscribe('starredMessages', rid, limit); + yield put(readyStarredMessages()); + if (sub) { + sub.unsubscribe().catch(e => console.warn('openStarredMessagesRoom', e)); + } + sub = newSub; +}; + +const closeStarredMessagesRoom = function* closeStarredMessagesRoom() { + if (sub) { + yield sub.unsubscribe().catch(e => console.warn('closeStarredMessagesRoom sub', e)); + } + if (newSub) { + yield newSub.unsubscribe().catch(e => console.warn('closeStarredMessagesRoom newSub', e)); + } }; const root = function* root() { - yield takeLatest(types.STARRED_MESSAGES.OPEN, watchStarredMessagesRoom); + yield takeLatest(types.STARRED_MESSAGES.OPEN, openStarredMessagesRoom); + yield takeLatest(types.STARRED_MESSAGES.CLOSE, closeStarredMessagesRoom); }; export default root; diff --git a/static/images/logo.png b/app/static/images/logo.png similarity index 100% rename from static/images/logo.png rename to app/static/images/logo.png diff --git a/static/images/logo_with_text.png b/app/static/images/logo_with_text.png similarity index 100% rename from static/images/logo_with_text.png rename to app/static/images/logo_with_text.png diff --git a/app/static/images/planet.png b/app/static/images/planet.png new file mode 100644 index 000000000..29989ae75 Binary files /dev/null and b/app/static/images/planet.png differ diff --git a/app/utils/throttle.js b/app/utils/throttle.js index adf492a56..88751335f 100644 --- a/app/utils/throttle.js +++ b/app/utils/throttle.js @@ -2,7 +2,7 @@ export default function throttle(fn, threshhold = 250, scope) { let last; let deferTimer; - return (...args) => { + const _throttle = (...args) => { const context = scope || this; const now = +new Date(); @@ -19,4 +19,8 @@ export default function throttle(fn, threshhold = 250, scope) { fn.apply(context, args); } }; + + _throttle.stop = () => clearTimeout(deferTimer); + + return _throttle; } diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js index acf8a8ec4..8b8e350bc 100644 --- a/app/views/CreateChannelView.js +++ b/app/views/CreateChannelView.js @@ -1,18 +1,20 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { TextInput, View, Text, Switch, TouchableOpacity, SafeAreaView } from 'react-native'; -import Spinner from 'react-native-loading-spinner-overlay'; +import { View, Text, Switch, TouchableOpacity, SafeAreaView, ScrollView } from 'react-native'; +import RCTextInput from '../containers/TextInput'; +import Loading from '../containers/Loading'; import LoggedView from './View'; import { createChannelRequest } from '../actions/createChannel'; import styles from './Styles'; import KeyboardView from '../presentation/KeyboardView'; +import scrollPersistTaps from '../utils/scrollPersistTaps'; @connect( state => ({ createChannel: state.createChannel, - users: state.createChannel.users + users: state.selectedUsers.users }), dispatch => ({ create: data => dispatch(createChannelRequest(data)) @@ -84,52 +86,52 @@ export default class CreateChannelView extends LoggedView { render() { return ( - - Channel Name - this.setState({ channelName })} - autoCorrect={false} - returnKeyType='done' - autoCapitalize='none' - autoFocus - placeholder='Type the channel name here' - /> - {this.renderChannelNameError()} - {this.renderTypeSwitch()} - - {this.state.type ? ( - 'Everyone can access this channel' - ) : ( - 'Just invited people can access this channel' - )} - - this.submit()} - style={[styles.buttonContainer_white, styles.enabledButton]} - > - CREATE - - - + + + this.setState({ channelName })} + placeholder='Type the channel name here' + returnKeyType='done' + autoFocus + /> + {this.renderChannelNameError()} + {this.renderTypeSwitch()} + + {this.state.type ? ( + 'Everyone can access this channel' + ) : ( + 'Just invited people can access this channel' + )} + + this.submit()} + style={[ + styles.buttonContainer_white, + this.state.channelName.length === 0 || this.props.createChannel.isFetching + ? styles.disabledButton + : styles.enabledButton + ]} + > + CREATE + + + + ); } diff --git a/app/views/ForgotPasswordView.js b/app/views/ForgotPasswordView.js index caf2aa6b5..167bb5beb 100644 --- a/app/views/ForgotPasswordView.js +++ b/app/views/ForgotPasswordView.js @@ -1,17 +1,25 @@ import React from 'react'; -import Spinner from 'react-native-loading-spinner-overlay'; import PropTypes from 'prop-types'; -import { Text, TextInput, View, TouchableOpacity, SafeAreaView } from 'react-native'; +import { Text, View, SafeAreaView, ScrollView } from 'react-native'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import LoggedView from './View'; -import * as loginActions from '../actions/login'; +import { forgotPasswordInit, forgotPasswordRequest } from '../actions/login'; import KeyboardView from '../presentation/KeyboardView'; +import TextInput from '../containers/TextInput'; +import Button from '../containers/Button'; +import Loading from '../containers/Loading'; import styles from './Styles'; import { showErrorAlert } from '../utils/info'; +import scrollPersistTaps from '../utils/scrollPersistTaps'; -class ForgotPasswordView extends LoggedView { +@connect(state => ({ + login: state.login +}), dispatch => ({ + forgotPasswordInit: () => dispatch(forgotPasswordInit()), + forgotPasswordRequest: email => dispatch(forgotPasswordRequest(email)) +})) +export default class ForgotPasswordView extends LoggedView { static propTypes = { forgotPasswordInit: PropTypes.func.isRequired, forgotPasswordRequest: PropTypes.func.isRequired, @@ -58,10 +66,11 @@ class ForgotPasswordView extends LoggedView { } resetPassword = () => { - if (this.state.invalidEmail) { + const { email, invalidEmail } = this.state; + if (invalidEmail || !email) { return; } - this.props.forgotPasswordRequest(this.state.email); + this.props.forgotPasswordRequest(email); } backLogin = () => { @@ -74,47 +83,35 @@ class ForgotPasswordView extends LoggedView { contentContainerStyle={styles.container} keyboardVerticalOffset={128} > - - - - this.validate(email)} - keyboardType='email-address' - autoCorrect={false} - returnKeyType='next' - autoCapitalize='none' - underlineColorAndroid='transparent' - onSubmitEditing={() => this.resetPassword()} - placeholder='Email' - /> + + + + + this.validate(email)} + onSubmitEditing={() => this.resetPassword()} + /> - - RESET PASSWORD - + +