* Fabric iOS

* Fabric configured on iOS and Android

* - react-native-fabric configured

- login tracked

* README updated

* Run scripts from README updated

* README scripts

* get rooms and messages by rest

* user status

* more improves

* more improves

* send pong on timeout

* fix some methods

* more tests

* rest messages

* Room actions (#266)

* Toggle notifications

* Search messages

* Invite users

* Mute/Unmute users in room

* rocket.cat messages

* Room topic layout fixed

* Starred messages loading onEndReached

* Room actions onEndReached

* Unnecessary login request

* Login loading

* Login services fixed

* User presence layout

* ïmproves on room actions view

* Removed unnecessary data from SelectedUsersView

* load few messages on open room, search message improve

* fix loading messages forever

* Removed state from search

* Custom message time format

* secureTextEntry layout

* Reduce android app size

* Roles subscription fix

* Public routes navigation

* fix reconnect

* - New login/register, login, register

* proguard

* Login flux

* App init/restore

* Android layout fixes

* Multiple meteor connection requests fixed

* Nested attachments

* Nested attachments

* fix check status

* New login layout (#269)

* Public routes navigation

* New login/register, login, register

* Multiple meteor connection requests fixed

* Nested attachments

* Button component

* TextInput android layout fixed

* Register fixed

* Thinner close modal button

* Requests /me after login only one time

* Static images moved

* fix reconnect

* fix ddp

* fix custom emoji

* New message layout (#273)

* Grouping messages

* Message layout

* Users typing animation

* Image  attachment layout
This commit is contained in:
Guilherme Gazzo 2018-04-24 16:34:03 -03:00
parent 4e64780bdd
commit 557e485613
No known key found for this signature in database
GPG Key ID: 1F85C9AD922D0829
135 changed files with 24794 additions and 12680 deletions

0
.circleci/changelog.sh Normal file → Executable file
View File

View File

@ -28,7 +28,7 @@ jobs:
- run: - run:
name: Test name: Test
command: | command: |
npm test npm run test
- run: - run:
name: Codecov name: Codecov
@ -151,9 +151,7 @@ jobs:
name: Install NPM modules name: Install NPM modules
command: | command: |
rm -rf node_modules rm -rf node_modules
# npm install --save react-native@0.51
npm install npm install
# npm install react-native
- run: - run:
name: Fix known build error name: Fix known build error
@ -227,6 +225,7 @@ workflows:
filters: filters:
branches: branches:
only: only:
- beta
- develop - develop
- master - master
# - ios-testflight: # - ios-testflight:

View File

@ -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 $ git clone git@github.com:RocketChat/Rocket.Chat.ReactNative.git
$ cd Rocket.Chat.ReactNative $ cd Rocket.Chat.ReactNative
$ npm install -g react-native-cli $ npm install -g react-native-cli
$ yarn $ npm install
``` ```
- Configuration - Configuration
```bash ```bash
$ yarn fabric-ios --key="YOUR_API_KEY" --secret="YOUR_API_SECRET" $ npm run fabric-ios --key="YOUR_API_KEY" --secret="YOUR_API_SECRET"
$ yarn fabric-android --key="YOUR_API_KEY" --secret="YOUR_API_SECRET" $ npm run fabric-android --key="YOUR_API_KEY" --secret="YOUR_API_SECRET"
``` ```
- Run application - Run application
```bash ```bash
$ yarn ios $ npm run ios
``` ```
```bash ```bash
$ yarn android $ npm run android
``` ```
# Storybook # Storybook

View File

@ -46,7 +46,7 @@ exports[`render channel 1`] = `
}, },
Object { Object {
"backgroundColor": "#00BCD4", "backgroundColor": "#00BCD4",
"borderRadius": 4, "borderRadius": 2,
"height": 46, "height": 46,
"width": 46, "width": 46,
}, },
@ -147,10 +147,8 @@ exports[`render channel 1`] = `
style={ style={
Object { Object {
"alignItems": "flex-end", "alignItems": "flex-end",
"flex": 1,
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"width": "100%",
} }
} }
/> />
@ -206,7 +204,7 @@ exports[`render no icon 1`] = `
}, },
Object { Object {
"backgroundColor": "#3F51B5", "backgroundColor": "#3F51B5",
"borderRadius": 4, "borderRadius": 2,
"height": 46, "height": 46,
"width": 46, "width": 46,
}, },
@ -307,10 +305,8 @@ exports[`render no icon 1`] = `
style={ style={
Object { Object {
"alignItems": "flex-end", "alignItems": "flex-end",
"flex": 1,
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"width": "100%",
} }
} }
/> />
@ -366,7 +362,7 @@ exports[`render private group 1`] = `
}, },
Object { Object {
"backgroundColor": "#FF9800", "backgroundColor": "#FF9800",
"borderRadius": 4, "borderRadius": 2,
"height": 46, "height": 46,
"width": 46, "width": 46,
}, },
@ -467,10 +463,8 @@ exports[`render private group 1`] = `
style={ style={
Object { Object {
"alignItems": "flex-end", "alignItems": "flex-end",
"flex": 1,
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"width": "100%",
} }
} }
/> />
@ -527,7 +521,7 @@ exports[`render unread +999 1`] = `
}, },
Object { Object {
"backgroundColor": "#3F51B5", "backgroundColor": "#3F51B5",
"borderRadius": 4, "borderRadius": 2,
"height": 46, "height": 46,
"width": 46, "width": 46,
}, },
@ -561,13 +555,16 @@ exports[`render unread +999 1`] = `
"height": 16, "height": 16,
"width": 16, "width": 16,
}, },
Object { Array [
"borderColor": "#fff", Object {
"borderWidth": 3, "borderColor": "#fff",
"bottom": -3, "borderWidth": 3,
"position": "absolute", "bottom": -3,
"right": -3, "position": "absolute",
}, "right": -3,
},
undefined,
],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": "#cbced1",
}, },
@ -639,10 +636,8 @@ exports[`render unread +999 1`] = `
style={ style={
Object { Object {
"alignItems": "flex-end", "alignItems": "flex-end",
"flex": 1,
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"width": "100%",
} }
} }
> >
@ -721,7 +716,7 @@ exports[`render unread 1`] = `
}, },
Object { Object {
"backgroundColor": "#3F51B5", "backgroundColor": "#3F51B5",
"borderRadius": 4, "borderRadius": 2,
"height": 46, "height": 46,
"width": 46, "width": 46,
}, },
@ -755,13 +750,16 @@ exports[`render unread 1`] = `
"height": 16, "height": 16,
"width": 16, "width": 16,
}, },
Object { Array [
"borderColor": "#fff", Object {
"borderWidth": 3, "borderColor": "#fff",
"bottom": -3, "borderWidth": 3,
"position": "absolute", "bottom": -3,
"right": -3, "position": "absolute",
}, "right": -3,
},
undefined,
],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": "#cbced1",
}, },
@ -833,10 +831,8 @@ exports[`render unread 1`] = `
style={ style={
Object { Object {
"alignItems": "flex-end", "alignItems": "flex-end",
"flex": 1,
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"width": "100%",
} }
} }
> >
@ -915,7 +911,7 @@ exports[`renders correctly 1`] = `
}, },
Object { Object {
"backgroundColor": "#3F51B5", "backgroundColor": "#3F51B5",
"borderRadius": 4, "borderRadius": 2,
"height": 46, "height": 46,
"width": 46, "width": 46,
}, },
@ -949,13 +945,16 @@ exports[`renders correctly 1`] = `
"height": 16, "height": 16,
"width": 16, "width": 16,
}, },
Object { Array [
"borderColor": "#fff", Object {
"borderWidth": 3, "borderColor": "#fff",
"bottom": -3, "borderWidth": 3,
"position": "absolute", "bottom": -3,
"right": -3, "position": "absolute",
}, "right": -3,
},
undefined,
],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": "#cbced1",
}, },
@ -1027,10 +1026,8 @@ exports[`renders correctly 1`] = `
style={ style={
Object { Object {
"alignItems": "flex-end", "alignItems": "flex-end",
"flex": 1,
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"width": "100%",
} }
} }
/> />

View File

@ -12,7 +12,7 @@ exports[`Storyshots Avatar avatar 1`] = `
}, },
Object { Object {
"backgroundColor": "#3F51B5", "backgroundColor": "#3F51B5",
"borderRadius": 4, "borderRadius": 2,
"height": 25, "height": 25,
"width": 25, "width": 25,
}, },
@ -48,7 +48,7 @@ exports[`Storyshots Avatar avatar 1`] = `
}, },
Object { Object {
"backgroundColor": "#9C27B0", "backgroundColor": "#9C27B0",
"borderRadius": 4, "borderRadius": 2,
"height": 40, "height": 40,
"width": 40, "width": 40,
}, },
@ -84,7 +84,7 @@ exports[`Storyshots Avatar avatar 1`] = `
}, },
Object { Object {
"backgroundColor": "#9C27B0", "backgroundColor": "#9C27B0",
"borderRadius": 4, "borderRadius": 2,
"height": 30, "height": 30,
"width": 30, "width": 30,
}, },
@ -198,7 +198,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
}, },
Object { Object {
"backgroundColor": "#8BC34A", "backgroundColor": "#8BC34A",
"borderRadius": 4, "borderRadius": 2,
"height": 46, "height": 46,
"width": 46, "width": 46,
}, },
@ -232,13 +232,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
"height": 16, "height": 16,
"width": 16, "width": 16,
}, },
Object { Array [
"borderColor": "#fff", Object {
"borderWidth": 3, "borderColor": "#fff",
"bottom": -3, "borderWidth": 3,
"position": "absolute", "bottom": -3,
"right": -3, "position": "absolute",
}, "right": -3,
},
undefined,
],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": "#cbced1",
}, },
@ -310,10 +313,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
style={ style={
Object { Object {
"alignItems": "flex-end", "alignItems": "flex-end",
"flex": 1,
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"width": "100%",
} }
} }
/> />
@ -364,7 +365,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
}, },
Object { Object {
"backgroundColor": "#8BC34A", "backgroundColor": "#8BC34A",
"borderRadius": 4, "borderRadius": 2,
"height": 46, "height": 46,
"width": 46, "width": 46,
}, },
@ -398,13 +399,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
"height": 16, "height": 16,
"width": 16, "width": 16,
}, },
Object { Array [
"borderColor": "#fff", Object {
"borderWidth": 3, "borderColor": "#fff",
"bottom": -3, "borderWidth": 3,
"position": "absolute", "bottom": -3,
"right": -3, "position": "absolute",
}, "right": -3,
},
undefined,
],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": "#cbced1",
}, },
@ -480,10 +484,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
style={ style={
Object { Object {
"alignItems": "flex-end", "alignItems": "flex-end",
"flex": 1,
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"width": "100%",
} }
} }
/> />
@ -534,7 +536,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
}, },
Object { Object {
"backgroundColor": "#8BC34A", "backgroundColor": "#8BC34A",
"borderRadius": 4, "borderRadius": 2,
"height": 46, "height": 46,
"width": 46, "width": 46,
}, },
@ -568,13 +570,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
"height": 16, "height": 16,
"width": 16, "width": 16,
}, },
Object { Array [
"borderColor": "#fff", Object {
"borderWidth": 3, "borderColor": "#fff",
"bottom": -3, "borderWidth": 3,
"position": "absolute", "bottom": -3,
"right": -3, "position": "absolute",
}, "right": -3,
},
undefined,
],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": "#cbced1",
}, },
@ -646,10 +651,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
style={ style={
Object { Object {
"alignItems": "flex-end", "alignItems": "flex-end",
"flex": 1,
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"width": "100%",
} }
} }
> >
@ -723,7 +726,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
}, },
Object { Object {
"backgroundColor": "#795548", "backgroundColor": "#795548",
"borderRadius": 4, "borderRadius": 2,
"height": 46, "height": 46,
"width": 46, "width": 46,
}, },
@ -757,13 +760,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
"height": 16, "height": 16,
"width": 16, "width": 16,
}, },
Object { Array [
"borderColor": "#fff", Object {
"borderWidth": 3, "borderColor": "#fff",
"bottom": -3, "borderWidth": 3,
"position": "absolute", "bottom": -3,
"right": -3, "position": "absolute",
}, "right": -3,
},
undefined,
],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": "#cbced1",
}, },
@ -839,10 +845,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
style={ style={
Object { Object {
"alignItems": "flex-end", "alignItems": "flex-end",
"flex": 1,
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"width": "100%",
} }
} }
> >
@ -916,7 +920,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
}, },
Object { Object {
"backgroundColor": "#795548", "backgroundColor": "#795548",
"borderRadius": 4, "borderRadius": 2,
"height": 46, "height": 46,
"width": 46, "width": 46,
}, },
@ -950,13 +954,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
"height": 16, "height": 16,
"width": 16, "width": 16,
}, },
Object { Array [
"borderColor": "#fff", Object {
"borderWidth": 3, "borderColor": "#fff",
"bottom": -3, "borderWidth": 3,
"position": "absolute", "bottom": -3,
"right": -3, "position": "absolute",
}, "right": -3,
},
undefined,
],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": "#cbced1",
}, },
@ -1028,10 +1035,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
style={ style={
Object { Object {
"alignItems": "flex-end", "alignItems": "flex-end",
"flex": 1,
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"width": "100%",
} }
} }
> >
@ -1105,7 +1110,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
}, },
Object { Object {
"backgroundColor": "#795548", "backgroundColor": "#795548",
"borderRadius": 4, "borderRadius": 2,
"height": 46, "height": 46,
"width": 46, "width": 46,
}, },
@ -1139,13 +1144,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
"height": 16, "height": 16,
"width": 16, "width": 16,
}, },
Object { Array [
"borderColor": "#fff", Object {
"borderWidth": 3, "borderColor": "#fff",
"bottom": -3, "borderWidth": 3,
"position": "absolute", "bottom": -3,
"right": -3, "position": "absolute",
}, "right": -3,
},
undefined,
],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": "#cbced1",
}, },
@ -1217,10 +1225,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
style={ style={
Object { Object {
"alignItems": "flex-end", "alignItems": "flex-end",
"flex": 1,
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"width": "100%",
} }
} }
> >
@ -1294,7 +1300,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
}, },
Object { Object {
"backgroundColor": "#795548", "backgroundColor": "#795548",
"borderRadius": 4, "borderRadius": 2,
"height": 46, "height": 46,
"width": 46, "width": 46,
}, },
@ -1328,13 +1334,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
"height": 16, "height": 16,
"width": 16, "width": 16,
}, },
Object { Array [
"borderColor": "#fff", Object {
"borderWidth": 3, "borderColor": "#fff",
"bottom": -3, "borderWidth": 3,
"position": "absolute", "bottom": -3,
"right": -3, "position": "absolute",
}, "right": -3,
},
undefined,
],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": "#cbced1",
}, },
@ -1406,10 +1415,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
style={ style={
Object { Object {
"alignItems": "flex-end", "alignItems": "flex-end",
"flex": 1,
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"width": "100%",
} }
} }
> >
@ -1483,7 +1490,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
}, },
Object { Object {
"backgroundColor": "#795548", "backgroundColor": "#795548",
"borderRadius": 4, "borderRadius": 2,
"height": 46, "height": 46,
"width": 46, "width": 46,
}, },
@ -1517,13 +1524,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
"height": 16, "height": 16,
"width": 16, "width": 16,
}, },
Object { Array [
"borderColor": "#fff", Object {
"borderWidth": 3, "borderColor": "#fff",
"bottom": -3, "borderWidth": 3,
"position": "absolute", "bottom": -3,
"right": -3, "position": "absolute",
}, "right": -3,
},
undefined,
],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": "#cbced1",
}, },
@ -1595,10 +1605,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
style={ style={
Object { Object {
"alignItems": "flex-end", "alignItems": "flex-end",
"flex": 1,
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"width": "100%",
} }
} }
> >
@ -1672,7 +1680,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
}, },
Object { Object {
"backgroundColor": "#E91E63", "backgroundColor": "#E91E63",
"borderRadius": 4, "borderRadius": 2,
"height": 46, "height": 46,
"width": 46, "width": 46,
}, },
@ -1706,13 +1714,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
"height": 16, "height": 16,
"width": 16, "width": 16,
}, },
Object { Array [
"borderColor": "#fff", Object {
"borderWidth": 3, "borderColor": "#fff",
"bottom": -3, "borderWidth": 3,
"position": "absolute", "bottom": -3,
"right": -3, "position": "absolute",
}, "right": -3,
},
undefined,
],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": "#cbced1",
}, },
@ -1784,10 +1795,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
style={ style={
Object { Object {
"alignItems": "flex-end", "alignItems": "flex-end",
"flex": 1,
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"width": "100%",
} }
} }
/> />
@ -1838,7 +1847,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
}, },
Object { Object {
"backgroundColor": "#9C27B0", "backgroundColor": "#9C27B0",
"borderRadius": 4, "borderRadius": 2,
"height": 46, "height": 46,
"width": 46, "width": 46,
}, },
@ -1872,13 +1881,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
"height": 16, "height": 16,
"width": 16, "width": 16,
}, },
Object { Array [
"borderColor": "#fff", Object {
"borderWidth": 3, "borderColor": "#fff",
"bottom": -3, "borderWidth": 3,
"position": "absolute", "bottom": -3,
"right": -3, "position": "absolute",
}, "right": -3,
},
undefined,
],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": "#cbced1",
}, },
@ -1950,10 +1962,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
style={ style={
Object { Object {
"alignItems": "flex-end", "alignItems": "flex-end",
"flex": 1,
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"width": "100%",
} }
} }
/> />
@ -2004,7 +2014,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
}, },
Object { Object {
"backgroundColor": undefined, "backgroundColor": undefined,
"borderRadius": 4, "borderRadius": 2,
"height": 46, "height": 46,
"width": 46, "width": 46,
}, },
@ -2038,13 +2048,16 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
"height": 16, "height": 16,
"width": 16, "width": 16,
}, },
Object { Array [
"borderColor": "#fff", Object {
"borderWidth": 3, "borderColor": "#fff",
"bottom": -3, "borderWidth": 3,
"position": "absolute", "bottom": -3,
"right": -3, "position": "absolute",
}, "right": -3,
},
undefined,
],
Object { Object {
"backgroundColor": "#cbced1", "backgroundColor": "#cbced1",
}, },
@ -2116,10 +2129,8 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
style={ style={
Object { Object {
"alignItems": "flex-end", "alignItems": "flex-end",
"flex": 1,
"flexDirection": "row", "flexDirection": "row",
"justifyContent": "flex-end", "justifyContent": "flex-end",
"width": "100%",
} }
} }
/> />

View File

@ -82,12 +82,12 @@ apply from: "../../node_modules/react-native/react.gradle"
* Upload all the APKs to the Play Store and people will download * Upload all the APKs to the Play Store and people will download
* the correct one based on the CPU architecture of their device. * 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. * Run Proguard to shrink the Java bytecode in release builds.
*/ */
def enableProguardInReleaseBuilds = false def enableProguardInReleaseBuilds = true
android { android {
compileSdkVersion 25 compileSdkVersion 25
@ -98,11 +98,12 @@ android {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 22 targetSdkVersion 22
versionCode VERSIONCODE as Integer versionCode VERSIONCODE as Integer
versionName "1.1" versionName "1"
ndk { ndk {
abiFilters "armeabi-v7a", "x86" abiFilters "armeabi-v7a", "x86"
} }
} }
signingConfigs { signingConfigs {
release { release {
if (project.hasProperty('KEYSTORE')) { if (project.hasProperty('KEYSTORE')) {
@ -123,10 +124,16 @@ android {
} }
buildTypes { buildTypes {
release { release {
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
signingConfig signingConfigs.release 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 are e.g. debug, release
applicationVariants.all { variant -> applicationVariants.all { variant ->

View File

@ -18,7 +18,7 @@
# Disabling obfuscation is useful if you collect stack traces from production crashes # 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). # (unless you are using a system that supports de-obfuscate the stack traces).
-dontobfuscate # -dontobfuscate
# React Native # React Native
@ -49,6 +49,7 @@
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup <methods>; } -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup <methods>; }
-dontwarn com.facebook.react.** -dontwarn com.facebook.react.**
-keep,includedescriptorclasses class com.facebook.react.bridge.** { *; }
# TextLayoutBuilder uses a non-public Android constructor within StaticLayout. # TextLayoutBuilder uses a non-public Android constructor within StaticLayout.
# See libs/proxy/src/main/java/com/facebook/fbui/textlayoutbuilder/proxy for details. # See libs/proxy/src/main/java/com/facebook/fbui/textlayoutbuilder/proxy for details.
@ -68,3 +69,25 @@
-dontwarn java.nio.file.* -dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.** -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 <methods>;
}
# 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.**

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="primary_dark">#660B0B0B</color> </resources>

View File

@ -0,0 +1,5 @@
<resources>
<string name="app_name">[DEVELOP] RocketChatRN</string>
<string name="no_browser_found">No Browser Found</string>
</resources>

View File

@ -0,0 +1,9 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:colorEdgeEffect">#aaaaaa</item>
</style>
</resources>

View File

@ -26,7 +26,9 @@
android:allowBackup="true" android:allowBackup="true"
android:label="@string/app_name" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme"
android:resizeableActivity="true"
android:largeHeap="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"

View File

@ -10,4 +10,6 @@ if (__DEV__) {
.use(reactotronRedux()) .use(reactotronRedux())
.use(sagaPlugin()) .use(sagaPlugin())
.connect(); .connect();
// Running on android device
// $ adb reverse tcp:9090 tcp:9090
} }

View File

@ -74,16 +74,8 @@ export const MESSAGES = createRequestTypes('MESSAGES', [
'CLEAR_INPUT', 'CLEAR_INPUT',
'TOGGLE_REACTION_PICKER' 'TOGGLE_REACTION_PICKER'
]); ]);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [ export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
...defaultTypes, export const SELECTED_USERS = createRequestTypes('SELECTED_USERS', ['ADD_USER', 'REMOVE_USER', 'RESET', 'SET_LOADING']);
'REQUEST_USERS',
'SUCCESS_USERS',
'FAILURE_USERS',
'SET_USERS',
'ADD_USER',
'REMOVE_USER',
'RESET'
]);
export const NAVIGATION = createRequestTypes('NAVIGATION', ['SET']); export const NAVIGATION = createRequestTypes('NAVIGATION', ['SET']);
export const SERVER = createRequestTypes('SERVER', [ export const SERVER = createRequestTypes('SERVER', [
...defaultTypes, ...defaultTypes,
@ -96,11 +88,11 @@ export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DI
export const LOGOUT = 'LOGOUT'; // logout is always success export const LOGOUT = 'LOGOUT'; // logout is always success
export const ACTIVE_USERS = createRequestTypes('ACTIVE_USERS', ['SET']); export const ACTIVE_USERS = createRequestTypes('ACTIVE_USERS', ['SET']);
export const ROLES = createRequestTypes('ROLES', ['SET']); export const ROLES = createRequestTypes('ROLES', ['SET']);
export const STARRED_MESSAGES = createRequestTypes('STARRED_MESSAGES', ['OPEN', 'CLOSE', 'MESSAGES_RECEIVED', 'MESSAGE_UNSTARRED']); export const STARRED_MESSAGES = createRequestTypes('STARRED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED', 'MESSAGE_UNSTARRED']);
export const PINNED_MESSAGES = createRequestTypes('PINNED_MESSAGES', ['OPEN', 'CLOSE', 'MESSAGES_RECEIVED', 'MESSAGE_UNPINNED']); export const PINNED_MESSAGES = createRequestTypes('PINNED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED', 'MESSAGE_UNPINNED']);
export const MENTIONED_MESSAGES = createRequestTypes('MENTIONED_MESSAGES', ['OPEN', 'CLOSE', 'MESSAGES_RECEIVED']); export const MENTIONED_MESSAGES = createRequestTypes('MENTIONED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'CLOSE', 'MESSAGES_RECEIVED']); export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
export const ROOM_FILES = createRequestTypes('ROOM_FILES', ['OPEN', 'CLOSE', 'MESSAGES_RECEIVED']); export const ROOM_FILES = createRequestTypes('ROOM_FILES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
export const INCREMENT = 'INCREMENT'; export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT'; export const DECREMENT = 'DECREMENT';

View File

@ -20,51 +20,3 @@ export function createChannelFailure(err) {
err err
}; };
} }
export function createChannelRequestUsers(data) {
return {
type: types.CREATE_CHANNEL.REQUEST_USERS,
data
};
}
export function createChannelSetUsers(data) {
return {
type: types.CREATE_CHANNEL.SET_USERS,
data
};
}
export function createChannelSuccessUsers(data) {
return {
type: types.CREATE_CHANNEL.SUCCESS_USERS,
data
};
}
export function createChannelFailureUsers(err) {
return {
type: types.CREATE_CHANNEL.FAILURE_USERS,
err
};
}
export function addUser(user) {
return {
type: types.CREATE_CHANNEL.ADD_USER,
user
};
}
export function removeUser(user) {
return {
type: types.CREATE_CHANNEL.REMOVE_USER,
user
};
}
export function reset() {
return {
type: types.CREATE_CHANNEL.RESET
};
}

View File

@ -1,12 +1,20 @@
import * as types from './actionsTypes'; import * as types from './actionsTypes';
export function openMentionedMessages(rid) { export function openMentionedMessages(rid, limit) {
return { return {
type: types.MENTIONED_MESSAGES.OPEN, type: types.MENTIONED_MESSAGES.OPEN,
rid rid,
limit
}; };
} }
export function readyMentionedMessages() {
return {
type: types.MENTIONED_MESSAGES.READY
};
}
export function closeMentionedMessages() { export function closeMentionedMessages() {
return { return {
type: types.MENTIONED_MESSAGES.CLOSE type: types.MENTIONED_MESSAGES.CLOSE

View File

@ -1,9 +1,9 @@
import * as types from './actionsTypes'; import * as types from './actionsTypes';
export function messagesRequest({ rid }) { export function messagesRequest(room) {
return { return {
type: types.MESSAGES.REQUEST, type: types.MESSAGES.REQUEST,
rid room
}; };
} }

View File

@ -1,12 +1,20 @@
import * as types from './actionsTypes'; import * as types from './actionsTypes';
export function openPinnedMessages(rid) { export function openPinnedMessages(rid, limit) {
return { return {
type: types.PINNED_MESSAGES.OPEN, type: types.PINNED_MESSAGES.OPEN,
rid rid,
limit
}; };
} }
export function readyPinnedMessages() {
return {
type: types.PINNED_MESSAGES.READY
};
}
export function closePinnedMessages() { export function closePinnedMessages() {
return { return {
type: types.PINNED_MESSAGES.CLOSE type: types.PINNED_MESSAGES.CLOSE

View File

@ -1,9 +1,16 @@
import * as types from './actionsTypes'; import * as types from './actionsTypes';
export function openRoomFiles(rid) { export function openRoomFiles(rid, limit) {
return { return {
type: types.ROOM_FILES.OPEN, type: types.ROOM_FILES.OPEN,
rid rid,
limit
};
}
export function readyRoomFiles() {
return {
type: types.ROOM_FILES.READY
}; };
} }

View File

@ -0,0 +1,29 @@
import * as types from './actionsTypes';
export function addUser(user) {
return {
type: types.SELECTED_USERS.ADD_USER,
user
};
}
export function removeUser(user) {
return {
type: types.SELECTED_USERS.REMOVE_USER,
user
};
}
export function reset() {
return {
type: types.SELECTED_USERS.RESET
};
}
export function setLoading(loading) {
return {
type: types.SELECTED_USERS.SET_LOADING,
loading
};
}

View File

@ -1,9 +1,16 @@
import * as types from './actionsTypes'; import * as types from './actionsTypes';
export function openSnippetedMessages(rid) { export function openSnippetedMessages(rid, limit) {
return { return {
type: types.SNIPPETED_MESSAGES.OPEN, type: types.SNIPPETED_MESSAGES.OPEN,
rid rid,
limit
};
}
export function readySnippetedMessages() {
return {
type: types.SNIPPETED_MESSAGES.READY
}; };
} }

View File

@ -1,9 +1,16 @@
import * as types from './actionsTypes'; import * as types from './actionsTypes';
export function openStarredMessages(rid) { export function openStarredMessages(rid, limit) {
return { return {
type: types.STARRED_MESSAGES.OPEN, type: types.STARRED_MESSAGES.OPEN,
rid rid,
limit
};
}
export function readyStarredMessages() {
return {
type: types.STARRED_MESSAGES.READY
}; };
} }

View File

@ -1,6 +1,8 @@
export const AVATAR_COLORS = ['#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFC107', '#FF9800', '#FF5722', '#795548', '#9E9E9E', '#607D8B']; export const AVATAR_COLORS = ['#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFC107', '#FF9800', '#FF5722', '#795548', '#9E9E9E', '#607D8B'];
export const ESLINT_FIX = null; export const ESLINT_FIX = null;
export const COLOR_DANGER = '#f5455c'; export const COLOR_DANGER = '#f5455c';
export const COLOR_BUTTON_PRIMARY = '#2D6AEA';
export const COLOR_TEXT = '#292E35';
export const STATUS_COLORS = { export const STATUS_COLORS = {
online: '#2de0a5', online: '#2de0a5',
busy: COLOR_DANGER, busy: COLOR_DANGER,

View File

@ -0,0 +1,12 @@
import React from 'react';
import { ActivityIndicator, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
indicator: {
padding: 10
}
});
const RCActivityIndicator = () => <ActivityIndicator style={styles.indicator} />;
export default RCActivityIndicator;

View File

@ -36,7 +36,7 @@ export default class Avatar extends React.PureComponent {
}; };
render() { render() {
const { const {
text = '', size = 25, baseUrl, borderRadius = 4, style, avatar, type = 'd' text = '', size = 25, baseUrl, borderRadius = 2, style, avatar, type = 'd'
} = this.props; } = this.props;
const { initials, color } = avatarInitialsAndColor(`${ text }`); const { initials, color } = avatarInitialsAndColor(`${ text }`);

View File

@ -6,11 +6,7 @@ import { connect } from 'react-redux';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
bannerContainer: { bannerContainer: {
backgroundColor: '#ddd', backgroundColor: '#ddd'
position: 'absolute',
top: '0%',
zIndex: 10,
width: '100%'
}, },
bannerText: { bannerText: {
textAlign: 'center', textAlign: 'center',
@ -21,7 +17,8 @@ const styles = StyleSheet.create({
@connect(state => ({ @connect(state => ({
connecting: state.meteor.connecting, connecting: state.meteor.connecting,
authenticating: state.login.isFetching, authenticating: state.login.isFetching,
offline: !state.meteor.connected offline: !state.meteor.connected,
logged: !!state.login.token
})) }))
export default class Banner extends React.PureComponent { export default class Banner extends React.PureComponent {
@ -31,7 +28,9 @@ export default class Banner extends React.PureComponent {
offline: PropTypes.bool offline: PropTypes.bool
} }
render() { render() {
const { connecting, authenticating, offline } = this.props; const {
connecting, authenticating, offline, logged
} = this.props;
if (offline) { if (offline) {
return ( return (
@ -40,6 +39,7 @@ export default class Banner extends React.PureComponent {
</View> </View>
); );
} }
if (connecting) { if (connecting) {
return ( return (
<View style={[styles.bannerContainer, { backgroundColor: '#0d0' }]}> <View style={[styles.bannerContainer, { backgroundColor: '#0d0' }]}>
@ -56,6 +56,14 @@ export default class Banner extends React.PureComponent {
); );
} }
return null; if (logged) {
return this.props.children;
}
return (
<View style={[styles.bannerContainer, { backgroundColor: 'orange' }]}>
<Text style={[styles.bannerText, { color: '#a00' }]}>Not logged...</Text>
</View>
);
} }
} }

View File

@ -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 (
<Touch
onPress={onPress}
accessibilityTraits='button'
style={Platform.OS === 'ios' && styles.margin}
disabled={disabled}
>
<View
style={[
styles.container,
styles[`background_${ type }`],
Platform.OS === 'android' && styles.margin,
disabled && styles.disabled
]}
>
<Text style={[styles.text, styles[`text_color_${ type }`]]}>{title}</Text>
</View>
</Touch>
);
}
}

View File

@ -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 (
<TouchableOpacity onPress={() => this.props.navigation.dispatch(NavigationActions.back())} style={styles.button}>
<Icon
style={styles.icon}
name='close'
size={25}
/>
</TouchableOpacity>
);
}
}

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { ScrollView } from 'react-native'; import { ScrollView } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view'; import ScrollableTabView from 'react-native-scrollable-tab-view';
import _ from 'lodash'; import map from 'lodash/map';
import { emojify } from 'react-emojione'; import { emojify } from 'react-emojione';
import TabBar from './TabBar'; import TabBar from './TabBar';
import EmojiCategory from './EmojiCategory'; import EmojiCategory from './EmojiCategory';
@ -78,7 +78,7 @@ export default class EmojiPicker extends Component {
return emojiRow.length ? emojiRow[0].count + 1 : 1; return emojiRow.length ? emojiRow[0].count + 1 : 1;
} }
updateFrequentlyUsed() { updateFrequentlyUsed() {
const frequentlyUsed = _.map(this.frequentlyUsed.slice(), (item) => { const frequentlyUsed = map(this.frequentlyUsed.slice(), (item) => {
if (item.isCustom) { if (item.isCustom) {
return item; return item;
} }
@ -88,7 +88,7 @@ export default class EmojiPicker extends Component {
} }
updateCustomEmojis() { updateCustomEmojis() {
const customEmojis = _.map(this.customEmojis.slice(), item => const customEmojis = map(this.customEmojis.slice(), item =>
({ content: item.name, extension: item.extension, isCustom: true })); ({ content: item.name, extension: item.extension, isCustom: true }));
this.setState({ customEmojis }); this.setState({ customEmojis });
} }

103
app/containers/Loading.js Normal file
View File

@ -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 (
<Modal
visible={this.props.visible}
transparent
onRequestClose={() => {}}
>
<View style={styles.container}>
<Animated.Image
source={require('../static/images/logo.png')}
style={[styles.image, {
opacity: this.state.opacity,
transform: [{
scale
}]
}]}
/>
</View>
</Modal>
);
}
}

View File

@ -80,6 +80,7 @@ export default class MessageBox extends React.PureComponent {
onChangeText(text) { onChangeText(text) {
this.setState({ text }); this.setState({ text });
this.props.typing(text.length > 0);
requestAnimationFrame(() => { requestAnimationFrame(() => {
const { start, end } = this.component._lastNativeSelection; const { start, end } = this.component._lastNativeSelection;
@ -174,11 +175,11 @@ export default class MessageBox extends React.PureComponent {
}; };
ImagePicker.showImagePicker(options, (response) => { ImagePicker.showImagePicker(options, (response) => {
if (response.didCancel) { if (response.didCancel) {
console.log('User cancelled image picker'); console.warn('User cancelled image picker');
} else if (response.error) { } else if (response.error) {
console.log('ImagePicker Error: ', response.error); console.warn('ImagePicker Error: ', response.error);
} else if (response.customButton) { } else if (response.customButton) {
console.log('User tapped custom button: ', response.customButton); console.warn('User tapped custom button: ', response.customButton);
} else { } else {
const fileInfo = { const fileInfo = {
name: response.fileName, name: response.fileName,
@ -278,7 +279,7 @@ export default class MessageBox extends React.PureComponent {
}); });
}); });
} catch (e) { } catch (e) {
console.log('spotlight canceled'); console.warn('spotlight canceled');
} finally { } finally {
delete this.oldPromise; delete this.oldPromise;
this.users = database.objects('users').filtered('username CONTAINS[c] $0', keyword).slice(); 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.roomsCache = [...this.roomsCache, ...results.rooms].filter(onlyUnique);
this.setState({ mentions: [...rooms.slice(), ...results.rooms] }); this.setState({ mentions: [...rooms.slice(), ...results.rooms] });
} catch (e) { } catch (e) {
console.log('spotlight canceled'); console.warn('spotlight canceled');
} finally { } finally {
delete this.oldPromise; delete this.oldPromise;
} }
@ -454,7 +455,6 @@ export default class MessageBox extends React.PureComponent {
style={{ margin: 8 }} style={{ margin: 8 }}
text={item.username || item.name} text={item.username || item.name}
size={30} size={30}
baseUrl={this.props.baseUrl}
/>, />,
<Text key='mention-item-name'>{ item.username || item.name }</Text> <Text key='mention-item-name'>{ item.username || item.name }</Text>
] ]

View File

@ -22,8 +22,9 @@ export default StyleSheet.create({
maxHeight: 120, maxHeight: 120,
flexGrow: 1, flexGrow: 1,
width: 1, width: 1,
paddingTop: 15, // paddingVertical: 12, needs to be paddingTop/paddingBottom because of iOS/Android's TextInput differences on rendering
paddingBottom: 15, paddingTop: 12,
paddingBottom: 12,
paddingLeft: 0, paddingLeft: 0,
paddingRight: 0 paddingRight: 0
}, },
@ -35,7 +36,7 @@ export default StyleSheet.create({
fontSize: 20, fontSize: 20,
textAlign: 'center', textAlign: 'center',
padding: 15, padding: 15,
paddingHorizontal: 21, paddingHorizontal: 12,
flex: 0 flex: 0
}, },
mentionList: { mentionList: {

View File

@ -1,26 +1,31 @@
import React from 'react'; 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 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 sharedStyles from '../views/Styles';
import { COLOR_DANGER } from '../constants/colors'; import { COLOR_DANGER, COLOR_TEXT } from '../constants/colors';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
inputContainer: { inputContainer: {
marginBottom: 20 marginBottom: 15
}, },
label: { label: {
marginBottom: 4, marginBottom: 10,
fontSize: 16 color: COLOR_TEXT,
fontSize: 14,
fontWeight: '700'
}, },
input: { input: {
fontSize: 14,
paddingTop: 12, paddingTop: 12,
paddingBottom: 12, paddingBottom: 12,
// paddingTop: 5,
// paddingBottom: 5,
paddingHorizontal: 10, paddingHorizontal: 10,
borderWidth: 2, borderWidth: 2,
borderRadius: 2, borderRadius: 4,
backgroundColor: 'white', backgroundColor: 'white',
borderColor: 'rgba(0,0,0,.15)', borderColor: 'rgba(0,0,0,.15)',
color: 'black' color: 'black'
@ -33,14 +38,23 @@ const styles = StyleSheet.create({
borderColor: COLOR_DANGER borderColor: COLOR_DANGER
}, },
wrap: { wrap: {
flex: 1,
position: 'relative' position: 'relative'
}, },
icon: { icon: {
position: 'absolute', position: 'absolute',
right: 0, color: 'rgba(0,0,0,.45)',
padding: 10, height: 45,
color: 'rgba(0,0,0,.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 = { static propTypes = {
label: PropTypes.string, label: PropTypes.string,
error: PropTypes.object, error: PropTypes.object,
secureTextEntry: PropTypes.bool secureTextEntry: PropTypes.bool,
containerStyle: ViewPropTypes.style,
inputStyle: PropTypes.object,
inputRef: PropTypes.func
} }
static defaultProps = { static defaultProps = {
error: {} error: {}
@ -58,28 +75,40 @@ export default class RCTextInput extends React.PureComponent {
showPassword: false showPassword: false
} }
get icon() { return <Icon name={this.state.showPassword ? 'eye-slash' : 'eye'} style={styles.icon} size={20} onPress={this.tooglePassword} />; } icon = ({ name, onPress, style }) => <Icon name={name} style={[styles.icon, style]} size={20} onPress={onPress} />
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() { render() {
const { const {
label, error, secureTextEntry, ...inputProps label, error, secureTextEntry, containerStyle, inputRef, iconLeft, inputStyle, ...inputProps
} = this.props; } = this.props;
const { showPassword } = this.state; const { showPassword } = this.state;
return ( return (
<View style={styles.inputContainer}> <View style={[styles.inputContainer, containerStyle]}>
{ label && <Text style={[styles.label, error.error && styles.labelError]}>{label}</Text> } { label && <Text style={[styles.label, error.error && styles.labelError]}>{label}</Text> }
<View style={styles.wrap}> <View style={styles.wrap}>
<TextInput <TextInput
style={[styles.input, error.error && styles.inputError]} style={[
styles.input,
error.error && styles.inputError,
inputStyle,
iconLeft && { paddingLeft: 40 },
secureTextEntry && { paddingRight: 40 }
]}
ref={inputRef}
autoCorrect={false} autoCorrect={false}
autoCapitalize='none' autoCapitalize='none'
underlineColorAndroid='transparent' underlineColorAndroid='transparent'
secureTextEntry={secureTextEntry && !showPassword} secureTextEntry={secureTextEntry && !showPassword}
{...inputProps} {...inputProps}
/> />
{secureTextEntry && this.icon} {iconLeft && this.iconLeft(iconLeft)}
{secureTextEntry && this.iconPassword(showPassword ? 'eye-off' : 'eye')}
</View> </View>
{error.error && <Text style={sharedStyles.error}>{error.reason}</Text>} {error.error && <Text style={sharedStyles.error}>{error.reason}</Text>}
</View> </View>

View File

@ -1,16 +1,17 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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'; import { connect } from 'react-redux';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
typing: { typing: {
transform: [{ scaleY: -1 }], transform: [{ scaleY: -1 }],
fontWeight: 'bold', fontWeight: 'bold',
paddingHorizontal: 15, paddingHorizontal: 15,
height: 25 height: 25
},
emptySpace: {
height: 5
} }
}); });
@ -18,11 +19,13 @@ const styles = StyleSheet.create({
username: state.login.user && state.login.user.username, username: state.login.user && state.login.user.username,
usersTyping: state.room.usersTyping usersTyping: state.room.usersTyping
})) }))
export default class Typing extends React.Component { export default class Typing extends React.Component {
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
return this.props.usersTyping.join() !== nextProps.usersTyping.join(); return this.props.usersTyping.join() !== nextProps.usersTyping.join();
} }
componentWillUpdate() {
LayoutAnimation.easeInEaseOut();
}
onPress = () => { onPress = () => {
Keyboard.dismiss(); Keyboard.dismiss();
} }
@ -31,7 +34,13 @@ export default class Typing extends React.Component {
return users.length ? `${ users.join(' ,') } ${ users.length > 1 ? 'are' : 'is' } typing` : ''; return users.length ? `${ users.join(' ,') } ${ users.length > 1 ? 'are' : 'is' } typing` : '';
} }
render() { render() {
return (<Text style={styles.typing} onPress={() => this.onPress()}>{this.usersTyping}</Text>); const { usersTyping } = this;
if (!usersTyping) {
return <View style={styles.emptySpace} />;
}
return (<Text style={styles.typing} onPress={() => this.onPress()}>{usersTyping}</Text>);
} }
} }

View File

@ -4,9 +4,9 @@ import { View, StyleSheet, TouchableOpacity, Text, Easing } from 'react-native';
import Video from 'react-native-video'; import Video from 'react-native-video';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import Slider from 'react-native-slider'; import Slider from 'react-native-slider';
import { connect } from 'react-redux';
import Markdown from './Markdown'; import Markdown from './Markdown';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
audioContainer: { audioContainer: {
flex: 1, flex: 1,
@ -61,6 +61,9 @@ const formatTime = (t = 0, duration = 0) => {
return `${ formattedMinutes }:${ formattedSeconds }`; return `${ formattedMinutes }:${ formattedSeconds }`;
}; };
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}))
export default class Audio extends React.PureComponent { export default class Audio extends React.PureComponent {
static propTypes = { static propTypes = {
file: PropTypes.object.isRequired, file: PropTypes.object.isRequired,
@ -115,8 +118,8 @@ export default class Audio extends React.PureComponent {
const { uri, paused } = this.state; const { uri, paused } = this.state;
const { description } = this.props.file; const { description } = this.props.file;
return ( return (
<View> [
<View style={styles.audioContainer}> <View key='audio' style={styles.audioContainer}>
<Video <Video
ref={(ref) => { ref={(ref) => {
this.player = ref; this.player = ref;
@ -154,9 +157,9 @@ export default class Audio extends React.PureComponent {
onValueChange={value => this.setState({ currentTime: value })} onValueChange={value => this.setState({ currentTime: value })}
/> />
</View> </View>
</View> </View>,
<Markdown msg={description} /> <Markdown key='description' msg={description} />
</View> ]
); );
} }
} }

View File

@ -2,8 +2,12 @@ import React from 'react';
import { Text, ViewPropTypes } from 'react-native'; import { Text, ViewPropTypes } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { emojify } from 'react-emojione'; import { emojify } from 'react-emojione';
import { connect } from 'react-redux';
import CustomEmoji from '../EmojiPicker/CustomEmoji'; import CustomEmoji from '../EmojiPicker/CustomEmoji';
@connect(state => ({
customEmojis: state.customEmojis
}))
export default class Emoji extends React.PureComponent { export default class Emoji extends React.PureComponent {
static propTypes = { static propTypes = {
content: PropTypes.string, content: PropTypes.string,

View File

@ -1,41 +1,30 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { CachedImage } from 'react-native-img-cache'; 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 PhotoModal from './PhotoModal';
import Markdown from './Markdown';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
flex: 1, flex: 1,
flexDirection: 'column', flexDirection: 'column'
height: 320,
borderColor: '#ccc',
borderWidth: 1,
borderRadius: 6
}, },
image: { image: {
flex: 1, width: 320,
height: undefined, height: 200,
width: undefined, resizeMode: 'cover'
resizeMode: 'contain'
}, },
labelContainer: { labelContainer: {
height: 62, alignItems: 'flex-start'
alignItems: 'center',
justifyContent: 'center'
},
imageName: {
fontSize: 12,
alignSelf: 'center',
fontStyle: 'italic'
},
message: {
alignSelf: 'center',
fontWeight: 'bold'
} }
}); });
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 = { static propTypes = {
file: PropTypes.object.isRequired, file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired, baseUrl: PropTypes.string.isRequired,
@ -45,8 +34,9 @@ export default class Image extends React.PureComponent {
state = { modalVisible: false }; state = { modalVisible: false };
getDescription() { getDescription() {
if (this.props.file.description) { const { file, customEmojis } = this.props;
return <Text style={styles.message}>{this.props.file.description}</Text>; if (file.description) {
return <Markdown msg={file.description} customEmojis={customEmojis} />;
} }
} }
@ -60,8 +50,9 @@ export default class Image extends React.PureComponent {
const { baseUrl, file, user } = this.props; const { baseUrl, file, user } = this.props;
const img = `${ baseUrl }${ file.image_url }?rc_uid=${ user.id }&rc_token=${ user.token }`; const img = `${ baseUrl }${ file.image_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
return ( return (
<View> [
<TouchableOpacity <TouchableOpacity
key='image'
onPress={() => this._onPressButton()} onPress={() => this._onPressButton()}
style={styles.button} style={styles.button}
> >
@ -69,18 +60,16 @@ export default class Image extends React.PureComponent {
style={styles.image} style={styles.image}
source={{ uri: encodeURI(img) }} source={{ uri: encodeURI(img) }}
/> />
<View style={styles.labelContainer}> {this.getDescription()}
<Text style={styles.imageName}>{this.props.file.title}</Text> </TouchableOpacity>,
{this.getDescription()}
</View>
</TouchableOpacity>
<PhotoModal <PhotoModal
key='modal'
title={this.props.file.title} title={this.props.file.title}
image={img} image={img}
isVisible={this.state.modalVisible} isVisible={this.state.modalVisible}
onClose={() => this.setState({ modalVisible: false })} onClose={() => this.setState({ modalVisible: false })}
/> />
</View> ]
); );
} }
} }

View File

@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import EasyMarkdown from 'react-native-easy-markdown'; // eslint-disable-line import EasyMarkdown from 'react-native-easy-markdown'; // eslint-disable-line
import SimpleMarkdown from 'simple-markdown'; import SimpleMarkdown from 'simple-markdown';
import { emojify } from 'react-emojione'; import { emojify } from 'react-emojione';
import { connect } from 'react-redux';
import styles from './styles'; import styles from './styles';
import CustomEmoji from '../EmojiPicker/CustomEmoji'; import CustomEmoji from '../EmojiPicker/CustomEmoji';
@ -17,126 +18,139 @@ const BlockCode = ({ node, state }) => (
); );
const mentionStyle = { color: '#13679a' }; const mentionStyle = { color: '#13679a' };
const Markdown = ({ const defaultRules = {
msg, customEmojis, style, markdownStyle, customRules, renderInline username: {
}) => { order: -1,
if (!msg) { match: SimpleMarkdown.inlineRegex(/^@[0-9a-zA-Z-_.]+/),
return null; parse: capture => ({ content: capture[0] }),
} react: (node, output, state) => ({
msg = emojify(msg, { output: 'unicode' }); type: 'custom',
key: state.key,
const defaultRules = { props: {
username: { children: (
order: -1, <Text
match: SimpleMarkdown.inlineRegex(/^@[0-9a-zA-Z-_.]+/), key={state.key}
parse: capture => ({ content: capture[0] }), style={mentionStyle}
react: (node, output, state) => ({ onPress={() => alert('Username')}
type: 'custom', >
key: state.key, {node.content}
props: { </Text>
children: ( )
<Text
key={state.key}
style={mentionStyle}
onPress={() => alert('Username')}
>
{node.content}
</Text>
)
}
})
},
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: (
<Text
key={state.key}
style={mentionStyle}
onPress={() => alert('Room')}
>
{node.content}
</Text>
)
}
})
},
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 key={state.key} node={node} state={state} />
)
}
})
},
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: (
<BlockCode key={state.key} node={node} state={state} />
)
}
})
},
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: <Text key={state.key}>{node.content[0]}</Text>
}
};
const content = node.content[1];
const emojiExtension = customEmojis[content];
if (emojiExtension) {
const emoji = { extension: emojiExtension, content };
element.props.children = (
<CustomEmoji key={state.key} style={styles.customEmoji} emoji={emoji} />
);
}
return element;
} }
} })
}; },
heading: {
const codeStyle = StyleSheet.flatten(styles.codeStyle); order: -2,
style = StyleSheet.flatten(style); match: SimpleMarkdown.inlineRegex(/^#[0-9a-zA-Z-_.]+/),
return ( parse: capture => ({ content: capture[0] }),
<EasyMarkdown react: (node, output, state) => ({
style={{ marginBottom: 0, ...style }} type: 'custom',
markdownStyles={{ code: codeStyle, ...markdownStyle }} key: state.key,
rules={{ ...defaultRules, ...customRules }} props: {
renderInline={renderInline} children: (
>{msg} <Text
</EasyMarkdown> key={state.key}
); style={mentionStyle}
onPress={() => alert('Room')}
>
{node.content}
</Text>
)
}
})
},
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 key={state.key} node={node} state={state} />
)
}
})
},
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: (
<BlockCode key={state.key} node={node} state={state} />
)
}
})
}
}; };
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 (
<EasyMarkdown
style={{ marginBottom: 0, ...s }}
markdownStyles={{ code: codeStyle, ...markdownStyle }}
rules={{
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: <Text key={state.key}>{node.content[0]}</Text>
}
};
const content = node.content[1];
const emojiExtension = customEmojis[content];
if (emojiExtension) {
const emoji = { extension: emojiExtension, content };
element.props.children = (
<CustomEmoji key={state.key} style={styles.customEmoji} emoji={emoji} />
);
}
return element;
}
},
...defaultRules,
...customRules
}}
renderInline={renderInline}
>{m}
</EasyMarkdown>
);
}
}
Markdown.propTypes = { Markdown.propTypes = {
msg: PropTypes.string.isRequired, msg: PropTypes.string,
customEmojis: PropTypes.object, customEmojis: PropTypes.object,
// eslint-disable-next-line react/no-typos // eslint-disable-next-line react/no-typos
style: ViewPropTypes.style, style: ViewPropTypes.style,
@ -149,5 +163,3 @@ BlockCode.propTypes = {
node: PropTypes.object, node: PropTypes.object,
state: PropTypes.object state: PropTypes.object
}; };
export default Markdown;

View File

@ -3,6 +3,7 @@ import { View, Text, TouchableWithoutFeedback, FlatList, StyleSheet } from 'reac
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import { connect } from 'react-redux';
import Emoji from './Emoji'; import Emoji from './Emoji';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -52,6 +53,10 @@ const styles = StyleSheet.create({
}); });
const standardEmojiStyle = { fontSize: 20 }; const standardEmojiStyle = { fontSize: 20 };
const customEmojiStyle = { width: 20, height: 20 }; const customEmojiStyle = { width: 20, height: 20 };
@connect(state => ({
customEmojis: state.customEmojis
}))
export default class ReactionsModal extends React.PureComponent { export default class ReactionsModal extends React.PureComponent {
static propTypes = { static propTypes = {
isVisible: PropTypes.bool.isRequired, isVisible: PropTypes.bool.isRequired,

View File

@ -7,7 +7,9 @@ import Avatar from '../Avatar';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
username: { username: {
fontWeight: 'bold' color: '#000',
fontWeight: '400',
fontSize: 14
}, },
usernameView: { usernameView: {
flexDirection: 'row', flexDirection: 'row',
@ -22,7 +24,8 @@ const styles = StyleSheet.create({
time: { time: {
fontSize: 10, fontSize: 10,
color: '#888', color: '#888',
paddingLeft: 5 paddingLeft: 5,
fontWeight: '400'
}, },
edited: { edited: {
marginLeft: 5, marginLeft: 5,
@ -35,11 +38,10 @@ export default class User extends React.PureComponent {
static propTypes = { static propTypes = {
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
Message_TimeFormat: PropTypes.string.isRequired, Message_TimeFormat: PropTypes.string.isRequired,
onPress: PropTypes.func, onPress: PropTypes.func
baseUrl: PropTypes.string
} }
renderEdited(item) { renderEdited = (item) => {
if (!item.editedBy) { if (!item.editedBy) {
return null; return null;
} }
@ -50,7 +52,6 @@ export default class User extends React.PureComponent {
style={{ marginLeft: 5 }} style={{ marginLeft: 5 }}
text={item.editedBy.username} text={item.editedBy.username}
size={20} size={20}
baseUrl={this.props.baseUrl}
avatar={item.avatar} avatar={item.avatar}
/> />
</View> </View>

View File

@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 Modal from 'react-native-modal';
import VideoPlayer from 'react-native-video-controls'; import VideoPlayer from 'react-native-video-controls';
import { connect } from 'react-redux';
import Markdown from './Markdown'; import Markdown from './Markdown';
import openLink from '../../utils/openLink'; 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 { export default class Video extends React.PureComponent {
static propTypes = { static propTypes = {
file: PropTypes.object.isRequired, file: PropTypes.object.isRequired,
@ -55,18 +59,20 @@ export default class Video extends React.PureComponent {
const { baseUrl, user } = this.props; const { baseUrl, user } = this.props;
const uri = `${ baseUrl }${ video_url }?rc_uid=${ user.id }&rc_token=${ user.token }`; const uri = `${ baseUrl }${ video_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
return ( return (
<View> [
<TouchableOpacity <TouchableOpacity
key='button'
style={styles.container} style={styles.container}
onPress={() => this.open()} onPress={() => this.open()}
> >
<Image <Image
source={require('../../../static/images/logo.png')} source={require('../../static/images/logo.png')}
style={styles.image} style={styles.image}
/> />
<Markdown msg={description} /> <Markdown msg={description} />
</TouchableOpacity> </TouchableOpacity>,
<Modal <Modal
key='modal'
isVisible={isVisible} isVisible={isVisible}
style={styles.modal} style={styles.modal}
supportedOrientations={['portrait', 'landscape']} supportedOrientations={['portrait', 'landscape']}
@ -78,7 +84,7 @@ export default class Video extends React.PureComponent {
disableVolume disableVolume
/> />
</Modal> </Modal>
</View> ]
); );
} }
} }

View File

@ -1,13 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/MaterialIcons'; import Icon from 'react-native-vector-icons/MaterialIcons';
import moment from 'moment'; import moment from 'moment';
import equal from 'deep-equal'; import equal from 'deep-equal';
import { KeyboardUtils } from 'react-native-keyboard-input'; import { KeyboardUtils } from 'react-native-keyboard-input';
import { actionsShow, errorActionsShow, toggleReactionPicker } from '../../actions/messages';
import Image from './Image'; import Image from './Image';
import User from './User'; import User from './User';
import Avatar from '../Avatar'; import Avatar from '../Avatar';
@ -18,13 +17,54 @@ import Url from './Url';
import Reply from './Reply'; import Reply from './Reply';
import ReactionsModal from './ReactionsModal'; import ReactionsModal from './ReactionsModal';
import Emoji from './Emoji'; import Emoji from './Emoji';
import messageStatus from '../../constants/messagesStatus';
import styles from './styles'; 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 => ({ @connect(state => ({
message: state.messages.message, message: state.messages.message,
editing: state.messages.editing, editing: state.messages.editing,
customEmojis: state.customEmojis customEmojis: state.customEmojis,
Message_TimeFormat: state.settings.Message_TimeFormat,
Message_GroupingPeriod: state.settings.Message_GroupingPeriod
}), dispatch => ({ }), dispatch => ({
actionsShow: actionMessage => dispatch(actionsShow(actionMessage)), actionsShow: actionMessage => dispatch(actionsShow(actionMessage)),
errorActionsShow: actionMessage => dispatch(errorActionsShow(actionMessage)), errorActionsShow: actionMessage => dispatch(errorActionsShow(actionMessage)),
@ -35,13 +75,13 @@ export default class Message extends React.Component {
status: PropTypes.any, status: PropTypes.any,
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
reactions: PropTypes.any.isRequired, reactions: PropTypes.any.isRequired,
baseUrl: PropTypes.string.isRequired,
Message_TimeFormat: PropTypes.string.isRequired, Message_TimeFormat: PropTypes.string.isRequired,
Message_GroupingPeriod: PropTypes.number.isRequired,
customTimeFormat: PropTypes.string,
message: PropTypes.object.isRequired, message: PropTypes.object.isRequired,
user: PropTypes.object.isRequired, user: PropTypes.object.isRequired,
editing: PropTypes.bool, editing: PropTypes.bool,
errorActionsShow: PropTypes.func, errorActionsShow: PropTypes.func,
customEmojis: PropTypes.object,
toggleReactionPicker: PropTypes.func, toggleReactionPicker: PropTypes.func,
onReactionPress: PropTypes.func, onReactionPress: PropTypes.func,
style: ViewPropTypes.style, style: ViewPropTypes.style,
@ -63,28 +103,35 @@ export default class Message extends React.Component {
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
if (!equal(this.props.reactions, nextProps.reactions)) {
return true;
}
if (this.state.reactionsModal !== nextState.reactionsModal) { if (this.state.reactionsModal !== nextState.reactionsModal) {
return true; 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 = () => { onPress = () => {
KeyboardUtils.dismiss(); KeyboardUtils.dismiss();
} }
onLongPress() { onLongPress = () => {
this.props.onLongPress(this.parseMessage()); this.props.onLongPress(this.parseMessage());
} }
onErrorPress() { onErrorPress = () => {
this.props.errorActionsShow(this.parseMessage()); this.props.errorActionsShow(this.parseMessage());
} }
onReactionPress(emoji) { onReactionPress = (emoji) => {
this.props.onReactionPress(emoji, this.props.item._id); this.props.onReactionPress(emoji, this.props.item._id);
} }
onClose() { onClose() {
@ -95,45 +142,9 @@ export default class Message extends React.Component {
Vibration.vibrate(50); Vibration.vibrate(50);
} }
getInfoMessage() { get timeFormat() {
let message = ''; const { customTimeFormat, Message_TimeFormat } = this.props;
const { return customTimeFormat || Message_TimeFormat;
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;
} }
parseMessage = () => JSON.parse(JSON.stringify(this.props.item)); parseMessage = () => JSON.parse(JSON.stringify(this.props.item));
@ -163,64 +174,97 @@ export default class Message extends React.Component {
} }
isTemp() { 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() { 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 (
<View style={styles.flex}>
<Avatar
style={styles.avatar}
text={item.avatar ? '' : username}
size={20}
avatar={item.avatar}
/>
<User
onPress={this._onPress}
item={item}
Message_TimeFormat={this.timeFormat}
/>
</View>
);
}
renderContent() {
if (this.isInfoMessage()) {
return <Text style={styles.textInfo}>{getInfoMessage(this.props.item)}</Text>;
}
const { item } = this.props;
return <Markdown msg={item.msg} />;
}
renderAttachment() {
if (this.props.item.attachments.length === 0) { if (this.props.item.attachments.length === 0) {
return null; return null;
} }
const file = this.props.item.attachments[0]; const file = this.props.item.attachments[0];
const { baseUrl, user } = this.props; const { user } = this.props;
if (file.image_type) { if (file.image_type) {
return <Image file={file} baseUrl={baseUrl} user={user} />; return <Image file={file} user={user} />;
} else if (file.audio_type) { }
return <Audio file={file} baseUrl={baseUrl} user={user} />; if (file.audio_type) {
} else if (file.video_type) { return <Audio file={file} user={user} />;
return <Video file={file} baseUrl={baseUrl} user={user} />; }
if (file.video_type) {
return <Video file={file} user={user} />;
} }
return <Reply attachment={file} timeFormat={this.props.Message_TimeFormat} />; return <Reply attachment={file} timeFormat={this.timeFormat} />;
} }
renderMessageContent() { renderUrl = () => {
if (this.isInfoMessage()) { const { urls } = this.props.item;
return <Text style={styles.textInfo}>{this.getInfoMessage()}</Text>; if (urls.length === 0) {
}
const { item, customEmojis, baseUrl } = this.props;
return <Markdown msg={item.msg} customEmojis={customEmojis} baseUrl={baseUrl} />;
}
renderUrl() {
if (this.props.item.urls.length === 0) {
return null; return null;
} }
return this.props.item.urls.map(url => ( return urls.map(url => (
<Url url={url} key={url.url} /> <Url url={url} key={url.url} />
)); ));
} };
renderError = () => { renderError = () => {
if (!this.hasError()) { if (!this.hasError()) {
return null; return null;
} }
return ( return (
<TouchableOpacity onPress={() => this.onErrorPress()}> <TouchableOpacity onPress={this.onErrorPress}>
<Icon name='error-outline' color='red' size={20} style={{ padding: 10, paddingRight: 12, paddingLeft: 0 }} /> <Icon name='error-outline' color='red' size={20} style={styles.errorIcon} />
</TouchableOpacity> </TouchableOpacity>
); );
} }
renderReaction(reaction) { renderReaction = (reaction) => {
const reacted = reaction.usernames.findIndex(item => item.value === this.props.user.username) !== -1; const reacted = reaction.usernames.findIndex(item => item.value === this.props.user.username) !== -1;
const reactedContainerStyle = reacted ? { borderColor: '#bde1fe', backgroundColor: '#f3f9ff' } : {}; const reactedContainerStyle = reacted && styles.reactedContainer;
const reactedCount = reacted ? { color: '#4fb0fc' } : {}; const reactedCount = reacted && styles.reactedCountText;
return ( return (
<TouchableOpacity <TouchableOpacity
onPress={() => this.onReactionPress(reaction.emoji)} onPress={() => this.onReactionPress(reaction.emoji)}
@ -232,7 +276,6 @@ export default class Message extends React.Component {
content={reaction.emoji} content={reaction.emoji}
standardEmojiStyle={styles.reactionEmoji} standardEmojiStyle={styles.reactionEmoji}
customEmojiStyle={styles.reactionCustomEmoji} customEmojiStyle={styles.reactionCustomEmoji}
customEmojis={this.props.customEmojis}
/> />
<Text style={[styles.reactionCount, reactedCount]}>{ reaction.usernames.length }</Text> <Text style={[styles.reactionCount, reactedCount]}>{ reaction.usernames.length }</Text>
</View> </View>
@ -246,7 +289,7 @@ export default class Message extends React.Component {
} }
return ( return (
<View style={styles.reactionsContainer}> <View style={styles.reactionsContainer}>
{this.props.item.reactions.map(reaction => this.renderReaction(reaction))} {this.props.item.reactions.map(this.renderReaction)}
<TouchableOpacity <TouchableOpacity
onPress={() => this.props.toggleReactionPicker(this.parseMessage())} onPress={() => this.props.toggleReactionPicker(this.parseMessage())}
key='add-reaction' key='add-reaction'
@ -260,57 +303,42 @@ export default class Message extends React.Component {
render() { render() {
const { const {
item, message, editing, baseUrl, customEmojis, style, archived item, message, editing, style, archived
} = this.props; } = this.props;
const username = item.alias || item.u.username; const username = item.alias || item.u.username;
const isEditing = message._id === item._id && editing; 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 ( return (
<TouchableHighlight <Touch
onPress={() => this.onPress()} onPress={this.onPress}
onLongPress={() => this.onLongPress()} onLongPress={this.onLongPress}
disabled={this.isDeleted() || this.hasError() || archived} disabled={this.isInfoMessage() || this.hasError() || archived}
underlayColor='#FFFFFF' underlayColor='#FFFFFF'
activeOpacity={0.3} activeOpacity={0.3}
style={[styles.message, isEditing ? styles.editing : null, style]}
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
> >
<View style={styles.flex}> <View style={[styles.message, isEditing && styles.editing, style]}>
{this.renderError()} {this.renderHeader(username)}
<View style={[this.isTemp() && { opacity: 0.3 }, styles.flex]}> <View style={styles.flex}>
<Avatar {this.renderError()}
style={styles.avatar} <View style={[styles.messageContent, this.isTemp() && styles.temp]}>
text={item.avatar ? '' : username} {this.renderContent()}
size={40} {this.renderAttachment()}
baseUrl={baseUrl}
avatar={item.avatar}
/>
<View style={[styles.content]}>
<User
onPress={this._onPress}
item={item}
Message_TimeFormat={this.props.Message_TimeFormat}
baseUrl={baseUrl}
/>
{this.renderMessageContent()}
{this.attachments()}
{this.renderUrl()} {this.renderUrl()}
{this.renderReactions()} {this.renderReactions()}
</View> </View>
</View> </View>
{this.state.reactionsModal ? {this.state.reactionsModal &&
<ReactionsModal <ReactionsModal
isVisible={this.state.reactionsModal} isVisible={this.state.reactionsModal}
onClose={this.onClose} onClose={this.onClose}
reactions={item.reactions} reactions={item.reactions}
user={this.props.user} user={this.props.user}
customEmojis={customEmojis}
/> />
: null
} }
</View> </View>
</TouchableHighlight> </Touch>
); );
} }
} }

View File

@ -1,20 +1,20 @@
import { StyleSheet, Platform } from 'react-native'; import { StyleSheet, Platform } from 'react-native';
export default StyleSheet.create({ export default StyleSheet.create({
content: { messageContent: {
flexGrow: 1, flex: 1,
flexShrink: 1 marginLeft: 30
}, },
flex: { flex: {
flexDirection: 'row', flexDirection: 'row',
flex: 1 flex: 1
}, },
message: { message: {
padding: 12, paddingHorizontal: 12,
paddingTop: 6, paddingVertical: 3,
paddingBottom: 6, flexDirection: 'column',
flexDirection: 'row', transform: [{ scaleY: -1 }],
transform: [{ scaleY: -1 }] flex: 1
}, },
textInfo: { textInfo: {
fontStyle: 'italic', fontStyle: 'italic',
@ -27,6 +27,7 @@ export default StyleSheet.create({
width: 16, width: 16,
height: 16 height: 16
}, },
temp: { opacity: 0.3 },
codeStyle: { codeStyle: {
...Platform.select({ ...Platform.select({
ios: { fontFamily: 'Courier New' }, ios: { fontFamily: 'Courier New' },
@ -40,7 +41,8 @@ export default StyleSheet.create({
}, },
reactionsContainer: { reactionsContainer: {
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap' flexWrap: 'wrap',
marginTop: 6
}, },
reactionContainer: { reactionContainer: {
flexDirection: 'row', flexDirection: 'row',
@ -70,5 +72,17 @@ export default StyleSheet.create({
}, },
avatar: { avatar: {
marginRight: 10 marginRight: 10
},
reactedContainer: {
borderColor: '#bde1fe',
backgroundColor: '#f3f9ff'
},
reactedCountText: {
color: '#4fb0fc'
},
errorIcon: {
padding: 10,
paddingRight: 12,
paddingLeft: 0
} }
}); });

View File

@ -6,12 +6,13 @@ import RoomsListView from '../../views/RoomsListView';
import RoomView from '../../views/RoomView'; import RoomView from '../../views/RoomView';
import RoomActionsView from '../../views/RoomActionsView'; import RoomActionsView from '../../views/RoomActionsView';
import CreateChannelView from '../../views/CreateChannelView'; import CreateChannelView from '../../views/CreateChannelView';
import SelectUsersView from '../../views/SelectUsersView'; import SelectedUsersView from '../../views/SelectedUsersView';
import NewServerView from '../../views/NewServerView'; import NewServerView from '../../views/NewServerView';
import StarredMessagesView from '../../views/StarredMessagesView'; import StarredMessagesView from '../../views/StarredMessagesView';
import PinnedMessagesView from '../../views/PinnedMessagesView'; import PinnedMessagesView from '../../views/PinnedMessagesView';
import MentionedMessagesView from '../../views/MentionedMessagesView'; import MentionedMessagesView from '../../views/MentionedMessagesView';
import SnippetedMessagesView from '../../views/SnippetedMessagesView'; import SnippetedMessagesView from '../../views/SnippetedMessagesView';
import SearchMessagesView from '../../views/SearchMessagesView';
import RoomFilesView from '../../views/RoomFilesView'; import RoomFilesView from '../../views/RoomFilesView';
import RoomMembersView from '../../views/RoomMembersView'; import RoomMembersView from '../../views/RoomMembersView';
import RoomInfoView from '../../views/RoomInfoView'; import RoomInfoView from '../../views/RoomInfoView';
@ -28,19 +29,22 @@ const AuthRoutes = StackNavigator(
CreateChannel: { CreateChannel: {
screen: CreateChannelView, screen: CreateChannelView,
navigationOptions: { navigationOptions: {
title: 'Create Channel' title: 'Create Channel',
headerTintColor: '#292E35'
} }
}, },
SelectUsers: { SelectedUsers: {
screen: SelectUsersView, screen: SelectedUsersView,
navigationOptions: { navigationOptions: {
title: 'Select Users' title: 'Select Users',
headerTintColor: '#292E35'
} }
}, },
AddServer: { AddServer: {
screen: NewServerView, screen: NewServerView,
navigationOptions: { navigationOptions: {
title: 'New server' title: 'New server',
headerTintColor: '#292E35'
} }
}, },
RoomActions: { RoomActions: {
@ -78,6 +82,13 @@ const AuthRoutes = StackNavigator(
headerTintColor: '#292E35' headerTintColor: '#292E35'
} }
}, },
SearchMessages: {
screen: SearchMessagesView,
navigationOptions: {
title: 'Search Messages',
headerTintColor: '#292E35'
}
},
RoomFiles: { RoomFiles: {
screen: RoomFilesView, screen: RoomFilesView,
navigationOptions: { navigationOptions: {

View File

@ -48,6 +48,11 @@ export function goRoom({ rid, name }, counter = 0) {
NavigationActions.navigate({ key: `Room-${ rid }`, routeName: 'Room', params: { room: { rid, name }, rid, name } }) NavigationActions.navigate({ key: `Room-${ rid }`, routeName: 'Room', params: { room: { rid, name }, rid, name } })
] ]
}); });
config.navigator.dispatch(action); config.navigator.dispatch(action);
} }
export function dispatch(action) {
if (config.navigator) {
config.navigator.dispatch(action);
}
}

View File

@ -5,74 +5,114 @@ import Icon from 'react-native-vector-icons/FontAwesome';
import ListServerView from '../../views/ListServerView'; import ListServerView from '../../views/ListServerView';
import NewServerView from '../../views/NewServerView'; import NewServerView from '../../views/NewServerView';
import LoginSignupView from '../../views/LoginSignupView';
import LoginView from '../../views/LoginView'; import LoginView from '../../views/LoginView';
import RegisterView from '../../views/RegisterView'; import RegisterView from '../../views/RegisterView';
import TermsServiceView from '../../views/TermsServiceView'; import TermsServiceView from '../../views/TermsServiceView';
import PrivacyPolicyView from '../../views/PrivacyPolicyView'; import PrivacyPolicyView from '../../views/PrivacyPolicyView';
import ForgotPasswordView from '../../views/ForgotPasswordView'; 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: (
<TouchableOpacity
onPress={() => navigation.navigate({ key: 'AddServer', routeName: 'AddServer' })}
style={{ width: 50, alignItems: 'center' }}
accessibilityLabel='Add server'
accessibilityTraits='button'
>
<Icon name='plus' size={16} />
</TouchableOpacity>
)
};
}
},
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( const PublicRoutes = StackNavigator(
{ {
ListServer: { Server: {
screen: ListServerView, screen: ServerStack
navigationOptions({ navigation }) {
return {
title: 'Servers',
headerRight: (
<TouchableOpacity
onPress={() => navigation.navigate({ key: 'AddServer', routeName: 'AddServer' })}
style={{ width: 50, alignItems: 'center' }}
accessibilityLabel='Add server'
accessibilityTraits='button'
>
<Icon name='plus' size={16} />
</TouchableOpacity>
)
};
}
},
AddServer: {
screen: NewServerView,
navigationOptions: {
title: 'New server'
}
}, },
Login: { Login: {
screen: LoginView, screen: LoginStack
navigationOptions: {
title: 'Login'
}
}, },
Register: { Register: {
screen: RegisterView, screen: RegisterStack
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'
}
} }
}, },
{ {
navigationOptions: { mode: 'modal',
headerTitleAllowFontScaling: false headerMode: 'none'
}
} }
); );

View File

@ -1,8 +1,8 @@
import { createStore as reduxCreateStore, applyMiddleware, compose } from 'redux'; import { createStore as reduxCreateStore, applyMiddleware, compose } from 'redux';
import Reactotron from 'reactotron-react-native' ; // eslint-disable-line
import createSagaMiddleware from 'redux-saga'; import createSagaMiddleware from 'redux-saga';
import logger from 'redux-logger';
import applyAppStateListener from 'redux-enhancer-react-native-appstate'; import applyAppStateListener from 'redux-enhancer-react-native-appstate';
import Reactotron from 'reactotron-react-native'; // eslint-disable-line
import reducers from '../reducers'; import reducers from '../reducers';
import sagas from '../sagas'; import sagas from '../sagas';
@ -20,8 +20,7 @@ if (__DEV__) {
enhancers = compose( enhancers = compose(
applyAppStateListener(), applyAppStateListener(),
applyMiddleware(reduxImmutableStateInvariant), applyMiddleware(reduxImmutableStateInvariant),
applyMiddleware(sagaMiddleware), applyMiddleware(sagaMiddleware)
applyMiddleware(logger)
); );
} else { } else {
sagaMiddleware = createSagaMiddleware(); sagaMiddleware = createSagaMiddleware();

View File

@ -1,4 +1,23 @@
import EJSON from 'ejson'; 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 { class EventEmitter {
constructor() { constructor() {
@ -9,6 +28,7 @@ class EventEmitter {
this.events[event] = []; this.events[event] = [];
} }
this.events[event].push(listener); this.events[event].push(listener);
return listener;
} }
removeListener(event, listener) { removeListener(event, listener) {
if (typeof this.events[event] === 'object') { if (typeof this.events[event] === 'object') {
@ -24,7 +44,8 @@ class EventEmitter {
try { try {
listener.apply(this, args); listener.apply(this, args);
} catch (e) { } catch (e) {
console.log(e); Answers.logCustom(e);
console.warn(e);
} }
}); });
} }
@ -34,72 +55,195 @@ class EventEmitter {
this.removeListener(event, g); this.removeListener(event, g);
listener.apply(this, args); listener.apply(this, args);
}); });
return listener;
} }
} }
export default class Socket extends EventEmitter { export default class Socket extends EventEmitter {
constructor(url) { constructor(url, login) {
super(); 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.id = 0;
this.subscriptions = {}; this.subscriptions = {};
this._connect();
this.ddp = new EventEmitter(); 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('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('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) => { return new Promise((resolve, reject) => {
this.id += 1; 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.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() { _connect() {
const connection = new WebSocket(`${ this.url }/websocket`); return new Promise((resolve) => {
connection.onopen = () => { this.lastping = new Date();
this.emit('open'); this._close();
this.send({ msg: 'connect', version: '1', support: ['1', 'pre2', 'pre1'] }); clearInterval(this.reconnect_timeout);
}; this.reconnect_timeout = setInterval(() => (!this.connection || this.connection.readyState > 1 || !this.check()) && this.reconnect(), 5000);
connection.onclose = e => this.emit('disconnected', e); this.connection = new WebSocket(`${ this.url }/websocket`, null);
// connection.onerror = () => {
// // alert(error.type);
// // console.log(error);
// // console.log(`WebSocket Error ${ JSON.stringify({...error}) }`);
// };
connection.onmessage = (e) => { this.connection.onopen = () => {
const data = EJSON.parse(e.data); this.emit('open');
this.emit(data.msg, data); resolve();
return data.collection && this.emit(data.collection, data); this.ddp.emit('open');
}; return this._login && this.login(this._login);
// this.on('disconnected', e => alert(JSON.stringify(e))); };
this.connection = connection; 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() { logout() {
this._login = null;
return this.call('logout').then(() => this.subscriptions = {}); return this.call('logout').then(() => this.subscriptions = {});
} }
disconnect() { disconnect() {
this.emit('disconnected_by_user'); this._close();
this.connection.close();
} }
reconnect() { async reconnect() {
this.disconnect(); if (this._timer) {
this.once('connected', () => { return;
Object.keys(this.subscriptions).forEach((key) => { }
const { name, params } = this.subscriptions[key]; delete this.connection;
this.subscriptions[key].unsubscribe(); this._logged = false;
this.subscribe(name, params);
}); this._timer = setTimeout(() => {
}); delete this._timer;
this._connect(); this._connect();
}, 1000);
} }
call(method, ...params) { call(method, ...params) {
return this.send({ return this.send({
msg: 'method', method, params 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) { unsubscribe(id) {
if (!this.subscriptions[id]) { if (!this.subscriptions[id]) {
@ -109,19 +253,31 @@ export default class Socket extends EventEmitter {
return this.send({ return this.send({
msg: 'unsub', msg: 'unsub',
id 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) { subscribe(name, ...params) {
console.log(name, params);
return this.send({ return this.send({
msg: 'sub', name, params msg: 'sub', name, params
}).then(({ id }) => { }).then(({ id }) => {
const args = { const args = {
id,
name, name,
params, params,
unsubscribe: () => this.unsubscribe(id) unsubscribe: () => this.unsubscribe(id)
}; };
this.subscriptions[id] = args; this.subscriptions[id] = args;
// console.log(args);
return args; return args;
}).catch((err) => {
console.warn('subscribe', err);
Answers.logCustom('DDP subscribe Error', err);
return Promise.reject(err);
}); });
} }
} }

View File

@ -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)));
}

View File

@ -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)));
}

View File

@ -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);
});
});
}

View File

@ -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)));
}

View File

@ -0,0 +1,7 @@
import normalizeMessage from './normalizeMessage';
import messagesStatus from '../../../constants/messagesStatus';
export default (message) => {
message.status = messagesStatus.SENT;
return normalizeMessage(message);
};

View File

@ -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
};
};

View File

@ -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;
};

View File

@ -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(/&amp;/g, '&');
}
tmp.image = decodedOgImage || meta.twitterImage || meta.oembedThumbnailUrl;
tmp.url = url.url;
return tmp;
});

View File

@ -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);
}
}
};

View File

@ -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);
};

View File

@ -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('&');
}

View File

@ -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([]);
});
}

View File

@ -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([]);
});
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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)
};
}

View File

@ -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);
}

View File

@ -51,6 +51,7 @@ const roomsSchema = {
_id: 'string', _id: 'string',
t: 'string', t: 'string',
lastMessage: 'messages', lastMessage: 'messages',
description: { type: 'string', optional: true },
_updatedAt: { type: 'date', optional: true } _updatedAt: { type: 'date', optional: true }
} }
}; };
@ -63,6 +64,14 @@ const subscriptionRolesSchema = {
} }
}; };
const userMutedInRoomSchema = {
name: 'usersMuted',
primaryKey: 'value',
properties: {
value: 'string'
}
};
const subscriptionSchema = { const subscriptionSchema = {
name: 'subscriptions', name: 'subscriptions',
primaryKey: '_id', primaryKey: '_id',
@ -90,7 +99,9 @@ const subscriptionSchema = {
blocked: { type: 'bool', optional: true }, blocked: { type: 'bool', optional: true },
reactWhenReadOnly: { type: 'bool', optional: true }, reactWhenReadOnly: { type: 'bool', optional: true },
archived: { 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 }, color: { type: 'string', optional: true },
ts: { type: 'date', optional: true }, ts: { type: 'date', optional: true },
attachments: { type: 'list', objectType: 'attachment' }, attachments: { type: 'list', objectType: 'attachment' },
fields: { type: 'list', objectType: 'attachmentFields' } fields: {
type: 'list', objectType: 'attachmentFields', default: []
}
} }
}; };
@ -265,8 +278,48 @@ const schema = [
customEmojisSchema, customEmojisSchema,
messagesReactionsSchema, messagesReactionsSchema,
messagesReactionsUsernamesSchema, 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 { class DB {
databases = { databases = {
serversDB: new Realm({ serversDB: new Realm({
@ -296,7 +349,7 @@ class DB {
return this.databases.activeDB; return this.databases.activeDB;
} }
setActiveDB(database) { setActiveDB(database = '') {
const path = database.replace(/(^\w+:|^)\/\//, ''); const path = database.replace(/(^\w+:|^)\/\//, '');
return this.databases.activeDB = new Realm({ return this.databases.activeDB = new Realm({
path: `${ path }.realm`, path: `${ path }.realm`,

View File

@ -1,47 +1,56 @@
import Random from 'react-native-meteor/lib/Random';
import { AsyncStorage, Platform } from 'react-native'; import { AsyncStorage, Platform } from 'react-native';
import { hashPassword } from 'react-native-meteor/lib/utils'; 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 RNFetchBlob from 'react-native-fetch-blob';
import reduxStore from './createStore'; import reduxStore from './createStore';
import settingsType from '../constants/settings'; import settingsType from '../constants/settings';
import messagesStatus from '../constants/messagesStatus'; import messagesStatus from '../constants/messagesStatus';
import database from './realm'; import database from './realm';
import * as actions from '../actions'; // import * as actions from '../actions';
import { someoneTyping, roomMessageReceived } from '../actions/room';
import { setUser, setLoginServices, removeLoginServices } from '../actions/login'; import { setUser, setLoginServices, removeLoginServices, loginRequest, loginSuccess, loginFailure } from '../actions/login';
import { disconnect, disconnect_by_user, connectSuccess, connectFailure } from '../actions/connect'; import { disconnect, connectSuccess, connectFailure } from '../actions/connect';
import { setActiveUser } from '../actions/activeUsers'; import { setActiveUser } from '../actions/activeUsers';
import { starredMessagesReceived, starredMessageUnstarred } from '../actions/starredMessages'; import { starredMessagesReceived, starredMessageUnstarred } from '../actions/starredMessages';
import { pinnedMessagesReceived, pinnedMessageUnpinned } from '../actions/pinnedMessages'; import { pinnedMessagesReceived, pinnedMessageUnpinned } from '../actions/pinnedMessages';
import { mentionedMessagesReceived } from '../actions/mentionedMessages'; import { mentionedMessagesReceived } from '../actions/mentionedMessages';
import { snippetedMessagesReceived } from '../actions/snippetedMessages'; import { snippetedMessagesReceived } from '../actions/snippetedMessages';
import { roomFilesReceived } from '../actions/roomFiles'; import { roomFilesReceived } from '../actions/roomFiles';
import { someoneTyping, roomMessageReceived } from '../actions/room';
import { setRoles } from '../actions/roles'; import { setRoles } from '../actions/roles';
import Ddp from './ddp'; 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 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 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 = { const RocketChat = {
TOKEN_KEY, TOKEN_KEY,
subscribeRooms,
subscribeRoom,
createChannel({ name, users, type }) { createChannel({ name, users, type }) {
return call(type ? 'createChannel' : 'createPrivateGroup', name, users, type); return call(type ? 'createChannel' : 'createPrivateGroup', name, users, type);
}, },
@ -97,59 +106,78 @@ const RocketChat = {
reduxStore.dispatch(setActiveUser(this.activeUsers)); reduxStore.dispatch(setActiveUser(this.activeUsers));
this._setUserTimer = null; this._setUserTimer = null;
return this.activeUsers = {}; return this.activeUsers = {};
}, 3000); }, 1000);
this.activeUsers[ddpMessage.id] = ddpMessage.fields; this.activeUsers[ddpMessage.id] = ddpMessage.fields;
}, },
reconnect() { async loginSuccess(user) {
if (this.ddp) { if (!user) {
this.ddp.reconnect(); 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) { connect(url, login) {
if (this.ddp) {
this.ddp.disconnect();
}
this.ddp = new Ddp(url);
return new Promise((resolve) => { return new Promise((resolve) => {
this.ddp.on('disconnected_by_user', () => { if (this.ddp) {
reduxStore.dispatch(disconnect_by_user()); this.ddp.disconnect();
}); delete this.ddp;
this.ddp.on('disconnected', () => { }
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()); 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) => { this.ddp.on('stream-room-messages', (ddpMessage) => {
const message = this._buildMessage(ddpMessage.fields.args[0]); const message = _buildMessage(ddpMessage.fields.args[0]);
return reduxStore.dispatch(roomMessageReceived(message)); 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('/'); const [_rid, ev] = ddpMessage.fields.eventName.split('/');
if (ev !== 'typing') { if (ev !== 'typing') {
return; return;
} }
return reduxStore.dispatch(someoneTyping({ _rid, username: ddpMessage.fields.args[0], typing: ddpMessage.fields.args[1] })); 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 [type, data] = ddpMessage.fields.args;
const [, ev] = ddpMessage.fields.eventName.split('/'); const [, ev] = ddpMessage.fields.eventName.split('/');
if (/subscriptions/.test(ev)) { if (/subscriptions/.test(ev)) {
@ -161,6 +189,11 @@ const RocketChat = {
} else { } else {
data.blocked = false; data.blocked = false;
} }
if (data.mobilePushNotifications === 'nothing') {
data.notifications = true;
} else {
data.notifications = false;
}
database.write(() => { database.write(() => {
database.create('subscriptions', data, true); database.create('subscriptions', data, true);
}); });
@ -178,11 +211,33 @@ const RocketChat = {
sub.reactWhenReadOnly = data.reactWhenReadOnly; sub.reactWhenReadOnly = data.reactWhenReadOnly;
sub.archived = data.archived; sub.archived = data.archived;
sub.joinCodeRequired = data.joinCodeRequired; 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') { if (ddpMessage.msg === 'added') {
this.starredMessages = this.starredMessages || []; this.starredMessages = this.starredMessages || [];
@ -191,14 +246,14 @@ const RocketChat = {
this.starredMessagesTimer = null; this.starredMessagesTimer = null;
} }
this.starredMessagesTimer = setTimeout(() => { this.starredMessagesTimer = setTimeout(protectedFunction(() => {
reduxStore.dispatch(starredMessagesReceived(this.starredMessages)); reduxStore.dispatch(starredMessagesReceived(this.starredMessages));
this.starredMessagesTimer = null; this.starredMessagesTimer = null;
return this.starredMessages = []; return this.starredMessages = [];
}, 1000); }), 1000);
const message = ddpMessage.fields; const message = ddpMessage.fields;
message._id = ddpMessage.id; message._id = ddpMessage.id;
const starredMessage = this._buildMessage(message); const starredMessage = _buildMessage(message);
this.starredMessages = [...this.starredMessages, starredMessage]; this.starredMessages = [...this.starredMessages, starredMessage];
} }
if (ddpMessage.msg === 'removed') { if (ddpMessage.msg === 'removed') {
@ -206,9 +261,9 @@ const RocketChat = {
return reduxStore.dispatch(starredMessageUnstarred(ddpMessage.id)); 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') { if (ddpMessage.msg === 'added') {
this.pinnedMessages = this.pinnedMessages || []; this.pinnedMessages = this.pinnedMessages || [];
@ -224,7 +279,7 @@ const RocketChat = {
}, 1000); }, 1000);
const message = ddpMessage.fields; const message = ddpMessage.fields;
message._id = ddpMessage.id; message._id = ddpMessage.id;
const pinnedMessage = this._buildMessage(message); const pinnedMessage = _buildMessage(message);
this.pinnedMessages = [...this.pinnedMessages, pinnedMessage]; this.pinnedMessages = [...this.pinnedMessages, pinnedMessage];
} }
if (ddpMessage.msg === 'removed') { if (ddpMessage.msg === 'removed') {
@ -232,9 +287,9 @@ const RocketChat = {
return reduxStore.dispatch(pinnedMessageUnpinned(ddpMessage.id)); 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') { if (ddpMessage.msg === 'added') {
this.mentionedMessages = this.mentionedMessages || []; this.mentionedMessages = this.mentionedMessages || [];
@ -250,12 +305,12 @@ const RocketChat = {
}, 1000); }, 1000);
const message = ddpMessage.fields; const message = ddpMessage.fields;
message._id = ddpMessage.id; message._id = ddpMessage.id;
const mentionedMessage = this._buildMessage(message); const mentionedMessage = _buildMessage(message);
this.mentionedMessages = [...this.mentionedMessages, mentionedMessage]; this.mentionedMessages = [...this.mentionedMessages, mentionedMessage];
} }
}); }));
this.ddp.on('rocketchat_snippeted_message', (ddpMessage) => { this.ddp.on('rocketchat_snippeted_message', protectedFunction((ddpMessage) => {
if (ddpMessage.msg === 'added') { if (ddpMessage.msg === 'added') {
this.snippetedMessages = this.snippetedMessages || []; this.snippetedMessages = this.snippetedMessages || [];
@ -271,12 +326,12 @@ const RocketChat = {
}, 1000); }, 1000);
const message = ddpMessage.fields; const message = ddpMessage.fields;
message._id = ddpMessage.id; message._id = ddpMessage.id;
const snippetedMessage = this._buildMessage(message); const snippetedMessage = _buildMessage(message);
this.snippetedMessages = [...this.snippetedMessages, snippetedMessage]; this.snippetedMessages = [...this.snippetedMessages, snippetedMessage];
} }
}); }));
this.ddp.on('room_files', (ddpMessage) => { this.ddp.on('room_files', protectedFunction((ddpMessage) => {
if (ddpMessage.msg === 'added') { if (ddpMessage.msg === 'added') {
this.roomFiles = this.roomFiles || []; this.roomFiles = this.roomFiles || [];
@ -318,9 +373,9 @@ const RocketChat = {
} }
this.roomFiles = [...this.roomFiles, message]; this.roomFiles = [...this.roomFiles, message];
} }
}); }));
this.ddp.on('meteor_accounts_loginServiceConfiguration', (ddpMessage) => { this.ddp.on('meteor_accounts_loginServiceConfiguration', protectedFunction((ddpMessage) => {
if (ddpMessage.msg === 'added') { if (ddpMessage.msg === 'added') {
this.loginServices = this.loginServices || {}; this.loginServices = this.loginServices || {};
if (this.loginServiceTimer) { if (this.loginServiceTimer) {
@ -340,9 +395,9 @@ const RocketChat = {
} }
this.loginServiceTimer = setTimeout(() => reduxStore.dispatch(removeLoginServices()), 1000); this.loginServiceTimer = setTimeout(() => reduxStore.dispatch(removeLoginServices()), 1000);
} }
}); }));
this.ddp.on('rocketchat_roles', (ddpMessage) => { this.ddp.on('rocketchat_roles', protectedFunction((ddpMessage) => {
this.roles = this.roles || {}; this.roles = this.roles || {};
if (this.roleTimer) { if (this.roleTimer) {
@ -353,39 +408,38 @@ const RocketChat = {
reduxStore.dispatch(setRoles(this.roles)); reduxStore.dispatch(setRoles(this.roles));
database.write(() => { database.write(() => {
_.forEach(this.roles, (description, _id) => { foreach(this.roles, (description, _id) => {
database.create('roles', { _id, description }, true); database.create('roles', { _id, description }, true);
}); });
}); });
this.roleTimer = null; this.roleTimer = null;
return this.roles = {}; return this.roles = {};
}, 5000); }, 1000);
this.roles[ddpMessage.id] = ddpMessage.fields.description; this.roles[ddpMessage.id] = (ddpMessage.fields && ddpMessage.fields.description) || undefined;
}); }));
}).catch(console.log);
},
me({ server, token, userId }) { this.ddp.on('error', protectedFunction((err) => {
return fetch(`${ server }/api/v1/me`, { console.warn('onError', JSON.stringify(err));
method: 'get', Answers.logCustom('disconnect', err);
headers: { reduxStore.dispatch(connectFailure());
'Content-Type': 'application/json', }));
'X-Auth-Token': token,
'X-User-Id': userId
}
}).then(response => response.json());
},
userInfo({ server, token, userId }) { // TODO: fix api (get emojis by date/version....)
return fetch(`${ server }/api/v1/users.info?userId=${ userId }`, {
method: 'get', this.ddp.on('open', protectedFunction(() => {
headers: { RocketChat.getSettings();
'Content-Type': 'application/json', RocketChat.getPermissions();
'X-Auth-Token': token, reduxStore.dispatch(connectSuccess());
'X-User-Id': userId resolve();
} }));
}).then(response => response.json());
this.ddp.once('open', protectedFunction(() => {
this.ddp.subscribe('activeUsers');
this.ddp.subscribe('roles');
RocketChat.getCustomEmoji();
}));
}).catch(err => console.warn(`asd ${ err }`));
}, },
register({ credentials }) { register({ credentials }) {
@ -442,19 +496,18 @@ const RocketChat = {
return this.login(params, callback); return this.login(params, callback);
}, },
loadSubscriptions(cb) { login(params) {
this.ddp.call('subscriptions/get').then((data) => { return this.ddp.login(params);
if (data.length) {
database.write(() => {
data.forEach((subscription) => {
database.create('subscriptions', subscription, true);
});
});
}
return cb && cb();
});
}, },
logout({ server }) {
if (this.ddp) {
this.ddp.logout();
}
database.deleteAll();
AsyncStorage.removeItem(TOKEN_KEY);
AsyncStorage.removeItem(`${ TOKEN_KEY }-${ server }`);
},
registerPushToken(id, token) { registerPushToken(id, token) {
const key = Platform.OS === 'ios' ? 'apn' : 'gcm'; const key = Platform.OS === 'ios' ? 'apn' : 'gcm';
const data = { const data = {
@ -470,92 +523,32 @@ const RocketChat = {
updatePushToken(pushId) { updatePushToken(pushId) {
return call('raix:push-setuser', pushId); return call('raix:push-setuser', pushId);
}, },
loadMissedMessages,
_parseUrls(urls) { loadMessagesForRoom,
return urls.filter(url => url.meta && !url.ignoreParse).map((url, index) => { getMessage,
const tmp = {}; sendMessage,
const { meta } = url; getRooms,
tmp._id = index; readMessages,
tmp.title = meta.ogTitle || meta.twitterTitle || meta.title || meta.pageTitle || meta.oembedTitle; me({ server = reduxStore.getState().server.server, token, userId }) {
tmp.description = meta.ogDescription || meta.twitterDescription || meta.description || meta.oembedAuthorName; return fetch(`${ server }/api/v1/me`, {
let decodedOgImage; method: 'get',
if (meta.ogImage) { headers: {
decodedOgImage = meta.ogImage.replace(/&amp;/g, '&'); 'Content-Type': 'application/json',
'X-Auth-Token': token,
'X-User-Id': userId
} }
tmp.image = decodedOgImage || meta.twitterImage || meta.oembedThumbnailUrl; }).then(response => response.json());
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);
}
});
}, },
getMessage(rid, msg = {}) { userInfo({ server = reduxStore.getState().server.server, token, userId }) {
const _id = Random.id(); return fetch(`${ server }/api/v1/users.info?userId=${ userId }`, {
const message = { method: 'get',
_id, headers: {
rid, 'Content-Type': 'application/json',
msg, 'X-Auth-Token': token,
ts: new Date(), 'X-User-Id': userId
_updatedAt: new Date(),
status: messagesStatus.TEMP,
u: {
_id: reduxStore.getState().login.user.id || '1',
username: reduxStore.getState().login.user.username
} }
}; }).then(response => response.json());
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);
}, },
async resendMessage(messageId) { async resendMessage(messageId) {
const message = await database.objects('messages').filtered('_id = $0', messageId)[0]; const message = await database.objects('messages').filtered('_id = $0', messageId)[0];
@ -563,7 +556,7 @@ const RocketChat = {
message.status = messagesStatus.TEMP; message.status = messagesStatus.TEMP;
database.create('messages', message, true); database.create('messages', message, true);
}); });
return RocketChat._sendMessageCall(message); return _sendMessageCall(JSON.parse(JSON.stringify(message)));
}, },
spotlight(search, usernames, type) { spotlight(search, usernames, type) {
@ -573,16 +566,6 @@ const RocketChat = {
createDirectMessage(username) { createDirectMessage(username) {
return call('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) { joinRoom(rid) {
return call('joinRoom', rid); return call('joinRoom', rid);
}, },
@ -646,6 +629,7 @@ const RocketChat = {
} catch (e) { } catch (e) {
return e; return e;
} finally { } finally {
// TODO: fix that
try { try {
database.write(() => { database.write(() => {
const msg = database.objects('messages').filtered('_id = $0', placeholder._id); const msg = database.objects('messages').filtered('_id = $0', placeholder._id);
@ -656,93 +640,9 @@ const RocketChat = {
} }
} }
}, },
async getRooms() { getSettings,
const { login } = reduxStore.getState(); getPermissions,
let lastMessage = database getCustomEmoji,
.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)));
},
parseSettings: settings => settings.reduce((ret, item) => { parseSettings: settings => settings.reduce((ret, item) => {
ret[item._id] = item[settingsType[item.type]] || item.valueAsString || item.valueAsNumber || ret[item._id] = item[settingsType[item.type]] || item.valueAsString || item.valueAsNumber ||
item.valueAsBoolean || item.value; item.valueAsBoolean || item.value;
@ -755,16 +655,6 @@ const RocketChat = {
}); });
}, },
_filterSettings: settings => settings.filter(setting => settingsType[setting.type] && setting.value), _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) => { parsePermissions: permissions => permissions.reduce((ret, item) => {
ret[item._id] = item.roles.reduce((roleRet, role) => [...roleRet, role.value], []); ret[item._id] = item.roles.reduce((roleRet, role) => [...roleRet, role.value], []);
return ret; return ret;
@ -775,16 +665,6 @@ const RocketChat = {
}); });
return permissions; 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) => { parseEmojis: emojis => emojis.reduce((ret, item) => {
ret[item.name] = item.extension; ret[item.name] = item.extension;
item.aliases.forEach((alias) => { item.aliases.forEach((alias) => {
@ -815,20 +695,21 @@ const RocketChat = {
return call('pinMessage', message); return call('pinMessage', message);
}, },
getRoom(rid) { getRoom(rid) {
const result = database.objects('subscriptions').filtered('rid = $0', rid); const [result] = database.objects('subscriptions').filtered('rid = $0', rid);
if (result.length === 0) { if (!result) {
return Promise.reject(new Error('Room not found')); return Promise.reject(new Error('Room not found'));
} }
return Promise.resolve(result[0]); return Promise.resolve(result);
}, },
async getPermalink(message) { async getPermalink(message) {
const room = await RocketChat.getRoom(message.rid); const room = await RocketChat.getRoom(message.rid);
const { server } = reduxStore.getState().server;
const roomType = { const roomType = {
p: 'group', p: 'group',
c: 'channel', c: 'channel',
d: 'direct' d: 'direct'
}[room.t]; }[room.t];
return `${ room._server.id }/${ roomType }/${ room.name }?msg=${ message._id }`; return `${ server }/${ roomType }/${ room.name }?msg=${ message._id }`;
}, },
subscribe(...args) { subscribe(...args) {
return this.ddp.subscribe(...args); return this.ddp.subscribe(...args);
@ -878,6 +759,12 @@ const RocketChat = {
eraseRoom(rid) { eraseRoom(rid) {
return call('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) { toggleArchiveRoom(rid, archive) {
if (archive) { if (archive) {
return call('archiveRoom', rid); return call('archiveRoom', rid);
@ -887,6 +774,17 @@ const RocketChat = {
saveRoomSettings(rid, params) { saveRoomSettings(rid, params) {
return call('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) { hasPermission(permissions, rid) {
// get the room from realm // get the room from realm
const room = database.objects('subscriptions').filtered('rid = $0', rid)[0]; const room = database.objects('subscriptions').filtered('rid = $0', rid)[0];

View File

@ -1,11 +1,13 @@
import React from 'react'; import React from 'react';
import moment from 'moment'; import moment from 'moment';
import PropTypes from 'prop-types'; 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 { connect } from 'react-redux';
import SimpleMarkdown from 'simple-markdown'; import SimpleMarkdown from 'simple-markdown';
import messagesStatus from '../constants/messagesStatus';
import Avatar from '../containers/Avatar'; import Avatar from '../containers/Avatar';
import Status from '../containers/status'; import Status from '../containers/status';
import Touch from '../utils/touch/index'; //eslint-disable-line import Touch from '../utils/touch/index'; //eslint-disable-line
@ -44,7 +46,6 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
fontSize: 18, fontSize: 18,
color: '#444', color: '#444',
marginRight: 8 marginRight: 8
}, },
lastMessage: { lastMessage: {
@ -64,8 +65,8 @@ const styles = StyleSheet.create({
// backgroundColor: '#eee' // backgroundColor: '#eee'
}, },
row: { row: {
width: '100%', // width: '100%',
flex: 1, // flex: 1,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'flex-end', alignItems: 'flex-end',
justifyContent: 'flex-end' justifyContent: 'flex-end'
@ -145,41 +146,57 @@ const renderNumber = (unread, userMentions) => {
); );
}; };
const attrs = ['name', 'unread', 'userMentions', 'alert', 'showLastMessage', 'type', '_updatedAt'];
@connect(state => ({ @connect(state => ({
user: state.login && state.login.user, user: state.login && state.login.user,
StoreLastMessage: state.settings.Store_Last_Message, StoreLastMessage: state.settings.Store_Last_Message
customEmojis: state.customEmojis
})) }))
export default class RoomItem extends React.PureComponent { export default class RoomItem extends React.Component {
static propTypes = { static propTypes = {
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
StoreLastMessage: PropTypes.bool, StoreLastMessage: PropTypes.bool,
_updatedAt: PropTypes.instanceOf(Date), _updatedAt: PropTypes.instanceOf(Date),
lastMessage: PropTypes.object, lastMessage: PropTypes.object,
showLastMessage: PropTypes.bool,
favorite: PropTypes.bool, favorite: PropTypes.bool,
alert: PropTypes.bool, alert: PropTypes.bool,
unread: PropTypes.number, unread: PropTypes.number,
userMentions: PropTypes.number, userMentions: PropTypes.number,
id: PropTypes.string, id: PropTypes.string,
onPress: PropTypes.func, onPress: PropTypes.func,
customEmojis: PropTypes.object, onLongPress: PropTypes.func,
user: PropTypes.object 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() { get icon() {
const { const {
type, name, id type, name, id, avatarSize, statusStyle
} = this.props; } = this.props;
return (<Avatar text={name} size={46} type={type}>{type === 'd' ? <Status style={styles.status} id={id} /> : null }</Avatar>); return (<Avatar text={name} size={avatarSize} type={type}>{type === 'd' ? <Status style={[styles.status, statusStyle]} id={id} /> : null }</Avatar>);
} }
get lastMessage() { get lastMessage() {
const { const {
lastMessage, type lastMessage, type, showLastMessage
} = this.props; } = this.props;
if (!this.props.StoreLastMessage) { if (!this.props.StoreLastMessage || !showLastMessage) {
return ''; return '';
} }
if (!lastMessage) { if (!lastMessage) {
@ -208,7 +225,7 @@ export default class RoomItem extends React.PureComponent {
render() { render() {
const { const {
favorite, unread, userMentions, name, _updatedAt, customEmojis, alert favorite, unread, userMentions, name, _updatedAt, alert, status
} = this.props; } = this.props;
const date = this.formatDate(_updatedAt); const date = this.formatDate(_updatedAt);
@ -224,10 +241,19 @@ export default class RoomItem extends React.PureComponent {
accessibilityLabel += ', you were mentioned'; accessibilityLabel += ', you were mentioned';
} }
accessibilityLabel += `, last message ${ date }`; if (date) {
accessibilityLabel += `, last message ${ date }`;
}
return ( return (
<Touch onPress={this.props.onPress} underlayColor='#FFFFFF' activeOpacity={0.5} accessibilityLabel={accessibilityLabel} accessibilityTraits='selected'> <Touch
onPress={this.props.onPress}
onLongPress={this.props.onLongPress}
underlayColor='#FFFFFF'
activeOpacity={0.5}
accessibilityLabel={accessibilityLabel}
accessibilityTraits='selected'
>
<View style={[styles.container, favorite && styles.favorite]}> <View style={[styles.container, favorite && styles.favorite]}>
{this.icon} {this.icon}
<View style={styles.roomNameView}> <View style={styles.roomNameView}>
@ -236,9 +262,9 @@ export default class RoomItem extends React.PureComponent {
{_updatedAt ? <Text style={[styles.update, alert && styles.updateAlert]} ellipsizeMode='tail' numberOfLines={1}>{ date }</Text> : null} {_updatedAt ? <Text style={[styles.update, alert && styles.updateAlert]} ellipsizeMode='tail' numberOfLines={1}>{ date }</Text> : null}
</View> </View>
<View style={styles.row}> <View style={styles.row}>
{status === messagesStatus.ERROR ? <Icon name='error-outline' color='red' size={12} style={{ marginRight: 5, alignSelf: 'center' }} /> : null }
<Markdown <Markdown
msg={this.lastMessage} msg={this.lastMessage}
customEmojis={customEmojis}
style={styles.lastMessage} style={styles.lastMessage}
markdownStyle={markdownStyle} markdownStyle={markdownStyle}
customRules={customRules} customRules={customRules}

View File

@ -3,7 +3,8 @@ import { CREATE_CHANNEL } from '../actions/actionsTypes';
const initialState = { const initialState = {
isFetching: false, isFetching: false,
failure: false, failure: false,
users: [] result: '',
error: ''
}; };
export default function messages(state = initialState, action) { export default function messages(state = initialState, action) {
@ -11,9 +12,9 @@ export default function messages(state = initialState, action) {
case CREATE_CHANNEL.REQUEST: case CREATE_CHANNEL.REQUEST:
return { return {
...state, ...state,
error: undefined, isFetching: true,
failure: false, failure: false,
isFetching: true error: ''
}; };
case CREATE_CHANNEL.SUCCESS: case CREATE_CHANNEL.SUCCESS:
return { return {
@ -29,18 +30,6 @@ export default function messages(state = initialState, action) {
failure: true, failure: true,
error: action.err error: action.err
}; };
case CREATE_CHANNEL.ADD_USER:
return {
...state,
users: state.users.concat(action.user)
};
case CREATE_CHANNEL.REMOVE_USER:
return {
...state,
users: state.users.filter(item => item.name !== action.user.name)
};
case CREATE_CHANNEL.RESET:
return initialState;
default: default:
return state; return state;
} }

View File

@ -7,6 +7,7 @@ import room from './room';
import rooms from './rooms'; import rooms from './rooms';
import server from './server'; import server from './server';
import navigator from './navigator'; import navigator from './navigator';
import selectedUsers from './selectedUsers';
import createChannel from './createChannel'; import createChannel from './createChannel';
import app from './app'; import app from './app';
import permissions from './permissions'; import permissions from './permissions';
@ -26,6 +27,7 @@ export default combineReducers({
messages, messages,
server, server,
navigator, navigator,
selectedUsers,
createChannel, createChannel,
app, app,
room, room,

View File

@ -1,11 +1,22 @@
import { MENTIONED_MESSAGES } from '../actions/actionsTypes'; import { MENTIONED_MESSAGES } from '../actions/actionsTypes';
const initialState = { const initialState = {
messages: [] messages: [],
ready: false
}; };
export default function server(state = initialState, action) { export default function server(state = initialState, action) {
switch (action.type) { switch (action.type) {
case MENTIONED_MESSAGES.OPEN:
return {
...state,
ready: false
};
case MENTIONED_MESSAGES.READY:
return {
...state,
ready: true
};
case MENTIONED_MESSAGES.MESSAGES_RECEIVED: case MENTIONED_MESSAGES.MESSAGES_RECEIVED:
return { return {
...state, ...state,

View File

@ -2,7 +2,8 @@ import { PINNED_MESSAGES } from '../actions/actionsTypes';
const initialState = { const initialState = {
messages: [], messages: [],
isOpen: false isOpen: false,
ready: false
}; };
export default function server(state = initialState, action) { export default function server(state = initialState, action) {
@ -10,7 +11,13 @@ export default function server(state = initialState, action) {
case PINNED_MESSAGES.OPEN: case PINNED_MESSAGES.OPEN:
return { return {
...state, ...state,
isOpen: true isOpen: true,
ready: false
};
case PINNED_MESSAGES.READY:
return {
...state,
ready: true
}; };
case PINNED_MESSAGES.MESSAGES_RECEIVED: case PINNED_MESSAGES.MESSAGES_RECEIVED:
return { return {

View File

@ -1,11 +1,22 @@
import { ROOM_FILES } from '../actions/actionsTypes'; import { ROOM_FILES } from '../actions/actionsTypes';
const initialState = { const initialState = {
messages: [] messages: [],
ready: false
}; };
export default function server(state = initialState, action) { export default function server(state = initialState, action) {
switch (action.type) { switch (action.type) {
case ROOM_FILES.OPEN:
return {
...state,
ready: false
};
case ROOM_FILES.READY:
return {
...state,
ready: true
};
case ROOM_FILES.MESSAGES_RECEIVED: case ROOM_FILES.MESSAGES_RECEIVED:
return { return {
...state, ...state,

View File

@ -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;
}
}

View File

@ -5,7 +5,8 @@ const initialState = {
connected: false, connected: false,
errorMessage: '', errorMessage: '',
failure: false, failure: false,
server: '' server: '',
adding: false
}; };
@ -32,8 +33,17 @@ export default function server(state = initialState, action) {
failure: true, failure: true,
errorMessage: action.err errorMessage: action.err
}; };
case SERVER.ADD:
return {
...state,
adding: true
};
case SERVER.SELECT: case SERVER.SELECT:
return { ...state, server: action.server }; return {
...state,
server: action.server,
adding: false
};
default: default:
return state; return state;
} }

View File

@ -1,11 +1,22 @@
import { SNIPPETED_MESSAGES } from '../actions/actionsTypes'; import { SNIPPETED_MESSAGES } from '../actions/actionsTypes';
const initialState = { const initialState = {
messages: [] messages: [],
ready: false
}; };
export default function server(state = initialState, action) { export default function server(state = initialState, action) {
switch (action.type) { switch (action.type) {
case SNIPPETED_MESSAGES.OPEN:
return {
...state,
ready: false
};
case SNIPPETED_MESSAGES.READY:
return {
...state,
ready: true
};
case SNIPPETED_MESSAGES.MESSAGES_RECEIVED: case SNIPPETED_MESSAGES.MESSAGES_RECEIVED:
return { return {
...state, ...state,

View File

@ -2,7 +2,8 @@ import { STARRED_MESSAGES } from '../actions/actionsTypes';
const initialState = { const initialState = {
messages: [], messages: [],
isOpen: false isOpen: false,
ready: false
}; };
export default function server(state = initialState, action) { export default function server(state = initialState, action) {
@ -10,7 +11,13 @@ export default function server(state = initialState, action) {
case STARRED_MESSAGES.OPEN: case STARRED_MESSAGES.OPEN:
return { return {
...state, ...state,
isOpen: true isOpen: true,
ready: false
};
case STARRED_MESSAGES.READY:
return {
...state,
ready: true
}; };
case STARRED_MESSAGES.MESSAGES_RECEIVED: case STARRED_MESSAGES.MESSAGES_RECEIVED:
return { return {

View File

@ -1,44 +1,44 @@
import { call, takeLatest, select, take, race } from 'redux-saga/effects'; import { call, takeLatest, select, put, all } from 'redux-saga/effects';
import { delay } from 'redux-saga'; import { AsyncStorage } from 'react-native';
import { METEOR } from '../actions/actionsTypes'; import { METEOR } from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { setToken } from '../actions/login';
const getServer = ({ server }) => server.server; const getServer = ({ server }) => server.server;
const getToken = function* getToken() {
const currentServer = yield select(getServer);
const connect = url => RocketChat.connect(url); const user = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`);
const watchConnect = function* watchConnect() { if (user) {
const { disconnect } = yield race({ yield put(setToken(JSON.parse(user)));
disconnect: take(METEOR.DISCONNECT), try {
disconnected_by_user: take(METEOR.DISCONNECT_BY_USER) yield call([AsyncStorage, 'setItem'], RocketChat.TOKEN_KEY, JSON.parse(user).token || '');
}); } catch (error) {
if (disconnect) { console.warn('getToken', error);
while (true) {
const { connected } = yield race({
connected: take(METEOR.SUCCESS),
timeout: call(delay, 1000)
});
if (connected) {
return;
}
yield RocketChat.reconnect();
} }
return JSON.parse(user);
} }
return yield put(setToken());
}; };
const connect = (...args) => RocketChat.connect(...args);
const test = function* test() { const test = function* test() {
// try { try {
const server = yield select(getServer); const server = yield select(getServer);
// const response = const user = yield call(getToken);
yield call(connect, server); // 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)); // yield put(connectSuccess(response));
// } catch (err) { } catch (err) {
console.warn('test', err);
// yield put(connectFailure(err.status)); // yield put(connectFailure(err.status));
// } }
}; };
const root = function* root() { const root = function* root() {
yield takeLatest(METEOR.REQUEST, test); yield takeLatest(METEOR.REQUEST, test);
// yield take(METEOR.SUCCESS, watchConnect); // yield take(METEOR.SUCCESS, watchConnect);
yield takeLatest(METEOR.SUCCESS, watchConnect); // yield takeLatest(METEOR.SUCCESS, watchConnect);
}; };
export default root; export default root;

View File

@ -28,4 +28,5 @@ const handleRequest = function* handleRequest({ data }) {
const root = function* root() { const root = function* root() {
yield takeLatest(CREATE_CHANNEL.REQUEST, handleRequest); yield takeLatest(CREATE_CHANNEL.REQUEST, handleRequest);
}; };
export default root; export default root;

View File

@ -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;

View File

@ -1,5 +1,4 @@
import { all } from 'redux-saga/effects'; import { all } from 'redux-saga/effects';
import hello from './hello';
import login from './login'; import login from './login';
import connect from './connect'; import connect from './connect';
import rooms from './rooms'; import rooms from './rooms';
@ -18,7 +17,6 @@ const root = function* root() {
yield all([ yield all([
init(), init(),
createChannel(), createChannel(),
hello(),
rooms(), rooms(),
login(), login(),
connect(), connect(),

View File

@ -2,15 +2,13 @@ import { AsyncStorage } from 'react-native';
import { call, put, takeLatest } from 'redux-saga/effects'; import { call, put, takeLatest } from 'redux-saga/effects';
import * as actions from '../actions'; import * as actions from '../actions';
import { setServer } from '../actions/server'; import { setServer } from '../actions/server';
import { restoreToken } from '../actions/login'; import { restoreToken, setUser } from '../actions/login';
import { APP } from '../actions/actionsTypes'; import { APP } from '../actions/actionsTypes';
import { setRoles } from '../actions/roles';
import database from '../lib/realm';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
const restore = function* restore() { const restore = function* restore() {
try { try {
const token = yield call([AsyncStorage, 'getItem'], 'reactnativemeteor_usertoken'); const token = yield call([AsyncStorage, 'getItem'], RocketChat.TOKEN_KEY);
if (token) { if (token) {
yield put(restoreToken(token)); yield put(restoreToken(token));
} }
@ -18,21 +16,16 @@ const restore = function* restore() {
const currentServer = yield call([AsyncStorage, 'getItem'], 'currentServer'); const currentServer = yield call([AsyncStorage, 'getItem'], 'currentServer');
if (currentServer) { if (currentServer) {
yield put(setServer(currentServer)); yield put(setServer(currentServer));
const settings = database.objects('settings');
yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length)))); const login = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`);
const permissions = database.objects('permissions'); if (login && login.user) {
yield put(actions.setAllPermissions(RocketChat.parsePermissions(permissions.slice(0, permissions.length)))); yield put(setUser(login.user));
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(actions.appReady({})); yield put(actions.appReady({}));
} catch (e) { } catch (e) {
console.log(e); console.warn('restore', e);
} }
}; };

View File

@ -1,16 +1,16 @@
import { AsyncStorage } from 'react-native'; import { AsyncStorage } from 'react-native';
import { put, call, takeLatest, select, all, take } from 'redux-saga/effects'; import { put, call, take, takeLatest, select, all } from 'redux-saga/effects';
import { Answers } from 'react-native-fabric';
import * as types from '../actions/actionsTypes'; import * as types from '../actions/actionsTypes';
import { import {
loginRequest, // loginRequest,
loginSubmit, // loginSubmit,
registerRequest, registerRequest,
registerIncomplete, registerIncomplete,
loginSuccess, // loginSuccess,
loginFailure, loginFailure,
logout, // logout,
setToken, // setToken,
registerSuccess, registerSuccess,
setUsernameRequest, setUsernameRequest,
setUsernameSuccess, setUsernameSuccess,
@ -23,40 +23,41 @@ import * as NavigationService from '../containers/routes/NavigationService';
const getUser = state => state.login; const getUser = state => state.login;
const getServer = state => state.server.server; const getServer = state => state.server.server;
const getIsConnected = state => state.meteor.connected; 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 registerCall = args => RocketChat.register(args);
const setUsernameCall = args => RocketChat.setUsername(args); const setUsernameCall = args => RocketChat.setUsername(args);
const loginSuccessCall = () => RocketChat.loginSuccess();
const logoutCall = args => RocketChat.logout(args); const logoutCall = args => RocketChat.logout(args);
const meCall = args => RocketChat.me(args);
const forgotPasswordCall = args => RocketChat.forgotPassword(args); const forgotPasswordCall = args => RocketChat.forgotPassword(args);
const userInfoCall = args => RocketChat.userInfo(args);
const getToken = function* getToken() { // const getToken = function* getToken() {
const currentServer = yield select(getServer); // const currentServer = yield select(getServer);
const user = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`); // const user = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`);
if (user) { // if (user) {
try { // try {
yield put(setToken(JSON.parse(user))); // yield put(setToken(JSON.parse(user)));
yield call([AsyncStorage, 'setItem'], RocketChat.TOKEN_KEY, JSON.parse(user).token || ''); // yield call([AsyncStorage, 'setItem'], RocketChat.TOKEN_KEY, JSON.parse(user).token || '');
return JSON.parse(user); // return JSON.parse(user);
} catch (e) { // } catch (e) {
console.log('getTokenerr', e); // console.log('getTokenerr', e);
} // }
} else { // } else {
return yield put(setToken()); // return yield put(setToken());
} // }
}; // };
const handleLoginWhenServerChanges = function* handleLoginWhenServerChanges() { // const handleLoginWhenServerChanges = function* handleLoginWhenServerChanges() {
try { // try {
const user = yield call(getToken); // const user = yield call(getToken);
if (user.token) { // if (user.token) {
yield put(loginRequest({ resume: user.token })); // yield put(loginRequest({ resume: user.token }));
} // }
} catch (e) { // } catch (e) {
console.log(e); // console.log(e);
} // }
}; // };
const saveToken = function* saveToken() { const saveToken = function* saveToken() {
const [server, user] = yield all([select(getServer), select(getUser)]); 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)); yield AsyncStorage.setItem(`${ RocketChat.TOKEN_KEY }-${ server }`, JSON.stringify(user));
const token = yield AsyncStorage.getItem('pushId'); const token = yield AsyncStorage.getItem('pushId');
if (token) { if (token) {
RocketChat.registerPushToken(user.user.id, token); yield RocketChat.registerPushToken(user.user.id, token);
} }
Answers.logLogin('Email', true, { server }); if (!user.user.username && !user.isRegistering) {
}; yield put(registerIncomplete());
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));
} }
}; };
const handleLoginSubmit = function* handleLoginSubmit({ credentials }) { // const handleLoginRequest = function* handleLoginRequest({ credentials }) {
yield put(loginRequest(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 }) { const handleRegisterSubmit = function* handleRegisterSubmit({ credentials }) {
yield put(registerRequest(credentials)); yield put(registerRequest(credentials));
@ -114,10 +103,14 @@ const handleRegisterRequest = function* handleRegisterRequest({ credentials }) {
}; };
const handleRegisterSuccess = function* handleRegisterSuccess({ credentials }) { const handleRegisterSuccess = function* handleRegisterSuccess({ credentials }) {
yield put(loginSubmit({ try {
username: credentials.email, yield call(loginCall, {
password: credentials.pass username: credentials.email,
})); password: credentials.pass
});
} catch (err) {
yield put(loginFailure(err));
}
}; };
const handleSetUsernameSubmit = function* handleSetUsernameSubmit({ credentials }) { const handleSetUsernameSubmit = function* handleSetUsernameSubmit({ credentials }) {
@ -128,6 +121,7 @@ const handleSetUsernameRequest = function* handleSetUsernameRequest({ credential
try { try {
yield call(setUsernameCall, { credentials }); yield call(setUsernameCall, { credentials });
yield put(setUsernameSuccess()); yield put(setUsernameSuccess());
yield call(loginSuccessCall);
} catch (err) { } catch (err) {
yield put(loginFailure(err)); yield put(loginFailure(err));
} }
@ -154,20 +148,24 @@ const handleForgotPasswordRequest = function* handleForgotPasswordRequest({ emai
}; };
const watchLoginOpen = function* watchLoginOpen() { const watchLoginOpen = function* watchLoginOpen() {
const isConnected = yield select(getIsConnected); try {
if (!isConnected) { const isConnected = yield select(getIsConnected);
yield take(types.METEOR.SUCCESS); 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() { const root = function* root() {
yield takeLatest(types.METEOR.SUCCESS, handleLoginWhenServerChanges); // yield takeLatest(types.METEOR.SUCCESS, handleLoginWhenServerChanges);
yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest); // yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest);
yield takeLatest(types.LOGIN.SUCCESS, saveToken); 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_REQUEST, handleRegisterRequest);
yield takeLatest(types.LOGIN.REGISTER_SUBMIT, handleRegisterSubmit); yield takeLatest(types.LOGIN.REGISTER_SUBMIT, handleRegisterSubmit);
yield takeLatest(types.LOGIN.REGISTER_SUCCESS, handleRegisterSuccess); yield takeLatest(types.LOGIN.REGISTER_SUCCESS, handleRegisterSuccess);

View File

@ -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 * as types from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { readyMentionedMessages } from '../actions/mentionedMessages';
const watchMentionedMessagesRoom = function* watchMentionedMessagesRoom({ rid }) { let sub;
const sub = yield RocketChat.subscribe('mentionedMessages', rid, 50); let newSub;
yield take(types.MENTIONED_MESSAGES.CLOSE);
sub.unsubscribe().catch(e => alert(e)); 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() { 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; export default root;

View File

@ -1,5 +1,5 @@
import { takeLatest, select, take, put, call } from 'redux-saga/effects'; import { takeLatest, put, call } from 'redux-saga/effects';
import { MESSAGES, LOGIN } from '../actions/actionsTypes'; import { MESSAGES } from '../actions/actionsTypes';
import { import {
messagesSuccess, messagesSuccess,
messagesFailure, messagesFailure,
@ -22,16 +22,16 @@ const toggleStarMessage = message => RocketChat.toggleStarMessage(message);
const getPermalink = message => RocketChat.getPermalink(message); const getPermalink = message => RocketChat.getPermalink(message);
const togglePinMessage = message => RocketChat.togglePinMessage(message); const togglePinMessage = message => RocketChat.togglePinMessage(message);
const get = function* get({ rid }) { const get = function* get({ room }) {
const auth = yield select(state => state.login.isAuthenticated);
if (!auth) {
yield take(LOGIN.SUCCESS);
}
try { try {
yield RocketChat.loadMessagesForRoom(rid, null); if (room.lastOpen) {
yield RocketChat.loadMissedMessages(room);
} else {
yield RocketChat.loadMessagesForRoom(room);
}
yield put(messagesSuccess()); yield put(messagesSuccess());
} catch (err) { } catch (err) {
console.log(err); console.warn('messagesFailure', err);
yield put(messagesFailure(err.status)); yield put(messagesFailure(err.status));
} }
}; };

View File

@ -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 * as types from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { readyPinnedMessages } from '../actions/pinnedMessages';
const watchPinnedMessagesRoom = function* watchPinnedMessagesRoom({ rid }) { let sub;
const sub = yield RocketChat.subscribe('pinnedMessages', rid, 50); let newSub;
yield take(types.PINNED_MESSAGES.CLOSE);
sub.unsubscribe().catch(e => alert(e)); 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() { 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; export default root;

View File

@ -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 * as types from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { readyRoomFiles } from '../actions/roomFiles';
const watchRoomFiles = function* watchRoomFiles({ rid }) { let sub;
const sub = yield RocketChat.subscribe('roomFiles', rid, 50); let newSub;
yield take(types.ROOM_FILES.CLOSE);
sub.unsubscribe().catch(e => alert(e)); 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() { 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; export default root;

View File

@ -1,9 +1,9 @@
import { Alert } from 'react-native'; import { Alert } from 'react-native';
import { put, call, takeLatest, take, select, race, fork, cancel, takeEvery } from 'redux-saga/effects'; import { put, call, takeLatest, take, select, race, fork, cancel, takeEvery } from 'redux-saga/effects';
import { delay } from 'redux-saga'; 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 * 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 { addUserTyping, removeUserTyping, setLastOpen } from '../actions/room';
import { messagesRequest } from '../actions/messages'; import { messagesRequest } from '../actions/messages';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
@ -13,18 +13,18 @@ import * as NavigationService from '../containers/routes/NavigationService';
const leaveRoom = rid => RocketChat.leaveRoom(rid); const leaveRoom = rid => RocketChat.leaveRoom(rid);
const eraseRoom = rid => RocketChat.eraseRoom(rid); const eraseRoom = rid => RocketChat.eraseRoom(rid);
const getRooms = function* getRooms() { // const getRooms = function* getRooms() {
return yield RocketChat.getRooms(); // return yield RocketChat.getRooms();
}; // };
const watchRoomsRequest = function* watchRoomsRequest() { // const watchRoomsRequest = function* watchRoomsRequest() {
try { // try {
yield call(getRooms); // yield call(getRooms);
yield put(roomsSuccess()); // yield put(roomsSuccess());
} catch (err) { // } catch (err) {
yield put(roomsFailure(err.status)); // yield put(roomsFailure(err.status));
} // }
}; // };
const cancelTyping = function* cancelTyping(username) { const cancelTyping = function* cancelTyping(username) {
while (true) { while (true) {
@ -50,45 +50,46 @@ const usersTyping = function* usersTyping({ rid }) {
} }
}; };
const handleMessageReceived = function* handleMessageReceived({ message }) { 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) { if (message.rid === room.rid) {
database.write(() => { database.write(() => {
database.create('messages', message, true); 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 watchRoomOpen = function* watchRoomOpen({ room }) {
const auth = yield select(state => state.login.isAuthenticated); yield put(messagesRequest({ ...room }));
if (!auth) { // const { open } = yield race({
yield take(types.LOGIN.SUCCESS); // 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); 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 }); const thread = yield fork(usersTyping, { rid: room.rid });
yield race({ yield race({
open: take(types.ROOM.OPEN), open: take(types.ROOM.OPEN),
close: take(types.ROOM.CLOSE) close: take(types.ROOM.CLOSE)
}); });
cancel(thread); cancel(thread);
subscriptions.forEach((sub) => { sub.stop();
sub.unsubscribe().catch(e => alert(e));
}); // subscriptions.forEach((sub) => {
// sub.unsubscribe().catch(e => alert(e));
// });
}; };
const watchuserTyping = function* watchuserTyping({ status }) { const watchuserTyping = function* watchuserTyping({ status }) {
@ -110,13 +111,13 @@ const watchuserTyping = function* watchuserTyping({ status }) {
} }
}; };
const updateRoom = function* updateRoom() { // const updateRoom = function* updateRoom() {
const room = yield select(state => state.room); // const room = yield select(state => state.room);
if (!room || !room.rid) { // if (!room || !room.rid) {
return; // return;
} // }
yield put(messagesRequest({ rid: room.rid })); // yield put(messagesRequest({ rid: room.rid }));
}; // };
const updateLastOpen = function* updateLastOpen() { const updateLastOpen = function* updateLastOpen() {
yield put(setLastOpen()); yield put(setLastOpen());
@ -157,11 +158,10 @@ const handleEraseRoom = function* handleEraseRoom({ rid }) {
const root = function* root() { const root = function* root() {
yield takeLatest(types.ROOM.USER_TYPING, watchuserTyping); yield takeLatest(types.ROOM.USER_TYPING, watchuserTyping);
yield takeLatest(types.LOGIN.SUCCESS, watchRoomsRequest);
yield takeLatest(types.ROOM.OPEN, watchRoomOpen); yield takeLatest(types.ROOM.OPEN, watchRoomOpen);
yield takeEvery(types.ROOM.MESSAGE_RECEIVED, handleMessageReceived); yield takeEvery(types.ROOM.MESSAGE_RECEIVED, handleMessageReceived);
yield takeLatest(FOREGROUND, updateRoom); // yield takeLatest(FOREGROUND, updateRoom);
yield takeLatest(FOREGROUND, watchRoomsRequest); // yield takeLatest(FOREGROUND, watchRoomsRequest);
yield takeLatest(BACKGROUND, updateLastOpen); yield takeLatest(BACKGROUND, updateLastOpen);
yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom); yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom);
yield takeLatest(types.ROOM.ERASE, handleEraseRoom); yield takeLatest(types.ROOM.ERASE, handleEraseRoom);

View File

@ -3,8 +3,9 @@ import { delay } from 'redux-saga';
import { AsyncStorage } from 'react-native'; import { AsyncStorage } from 'react-native';
import { SERVER } from '../actions/actionsTypes'; import { SERVER } from '../actions/actionsTypes';
import * as actions from '../actions'; import * as actions from '../actions';
import { connectRequest, disconnect, disconnect_by_user } from '../actions/connect'; import { connectRequest } from '../actions/connect';
import { changedServer, serverSuccess, serverFailure, serverRequest, setServer } from '../actions/server'; import { serverSuccess, serverFailure, serverRequest, setServer } from '../actions/server';
import { setRoles } from '../actions/roles';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/realm'; import database from '../lib/realm';
import * as NavigationService from '../containers/routes/NavigationService'; import * as NavigationService from '../containers/routes/NavigationService';
@ -14,16 +15,28 @@ const validate = function* validate(server) {
}; };
const selectServer = function* selectServer({ server }) { const selectServer = function* selectServer({ server }) {
yield database.setActiveDB(server); try {
yield put(disconnect_by_user()); yield database.setActiveDB(server);
yield put(disconnect());
yield put(changedServer(server)); // yield RocketChat.disconnect();
yield call([AsyncStorage, 'setItem'], 'currentServer', server);
const settings = database.objects('settings'); yield call([AsyncStorage, 'setItem'], 'currentServer', server);
yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length)))); const settings = database.objects('settings');
const permissions = database.objects('permissions'); yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length))));
yield put(actions.setAllPermissions(RocketChat.parsePermissions(permissions.slice(0, permissions.length)))); const permissions = database.objects('permissions');
yield put(connectRequest(server)); 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 }) { const validateServer = function* validateServer({ server }) {
@ -32,7 +45,7 @@ const validateServer = function* validateServer({ server }) {
yield call(validate, server); yield call(validate, server);
yield put(serverSuccess()); yield put(serverSuccess());
} catch (e) { } catch (e) {
console.log(e); console.warn('validateServer', e);
yield put(serverFailure(e)); yield put(serverFailure(e));
} }
}; };

View File

@ -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 * as types from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { readySnippetedMessages } from '../actions/snippetedMessages';
const watchSnippetedMessagesRoom = function* watchSnippetedMessagesRoom({ rid }) { let sub;
const sub = yield RocketChat.subscribe('snippetedMessages', rid, 50); let newSub;
yield take(types.SNIPPETED_MESSAGES.CLOSE);
sub.unsubscribe().catch(e => alert(e)); 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() { 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; export default root;

View File

@ -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 * as types from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { readyStarredMessages } from '../actions/starredMessages';
const watchStarredMessagesRoom = function* watchStarredMessagesRoom({ rid }) { let sub;
const sub = yield RocketChat.subscribe('starredMessages', rid, 50); let newSub;
yield take(types.STARRED_MESSAGES.CLOSE);
sub.unsubscribe().catch(e => alert(e)); 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() { 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; export default root;

View File

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -2,7 +2,7 @@ export default function throttle(fn, threshhold = 250, scope) {
let last; let last;
let deferTimer; let deferTimer;
return (...args) => { const _throttle = (...args) => {
const context = scope || this; const context = scope || this;
const now = +new Date(); const now = +new Date();
@ -19,4 +19,8 @@ export default function throttle(fn, threshhold = 250, scope) {
fn.apply(context, args); fn.apply(context, args);
} }
}; };
_throttle.stop = () => clearTimeout(deferTimer);
return _throttle;
} }

View File

@ -1,18 +1,20 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { TextInput, View, Text, Switch, TouchableOpacity, SafeAreaView } from 'react-native'; import { View, Text, Switch, TouchableOpacity, SafeAreaView, ScrollView } from 'react-native';
import Spinner from 'react-native-loading-spinner-overlay';
import RCTextInput from '../containers/TextInput';
import Loading from '../containers/Loading';
import LoggedView from './View'; import LoggedView from './View';
import { createChannelRequest } from '../actions/createChannel'; import { createChannelRequest } from '../actions/createChannel';
import styles from './Styles'; import styles from './Styles';
import KeyboardView from '../presentation/KeyboardView'; import KeyboardView from '../presentation/KeyboardView';
import scrollPersistTaps from '../utils/scrollPersistTaps';
@connect( @connect(
state => ({ state => ({
createChannel: state.createChannel, createChannel: state.createChannel,
users: state.createChannel.users users: state.selectedUsers.users
}), }),
dispatch => ({ dispatch => ({
create: data => dispatch(createChannelRequest(data)) create: data => dispatch(createChannelRequest(data))
@ -84,52 +86,52 @@ export default class CreateChannelView extends LoggedView {
render() { render() {
return ( return (
<KeyboardView <KeyboardView
style={[styles.defaultViewBackground, { flex: 1 }]} contentContainerStyle={styles.container}
contentContainerStyle={styles.defaultView} keyboardVerticalOffset={128}
> >
<SafeAreaView style={styles.formContainer}> <ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
<Text style={styles.label_white}>Channel Name</Text> <SafeAreaView>
<TextInput <RCTextInput
value={this.state.channelName} label='Channel Name'
style={styles.input_white} value={this.state.channelName}
onChangeText={channelName => this.setState({ channelName })} onChangeText={channelName => this.setState({ channelName })}
autoCorrect={false} placeholder='Type the channel name here'
returnKeyType='done' returnKeyType='done'
autoCapitalize='none' autoFocus
autoFocus />
placeholder='Type the channel name here' {this.renderChannelNameError()}
/> {this.renderTypeSwitch()}
{this.renderChannelNameError()} <Text
{this.renderTypeSwitch()} style={[
<Text styles.label_white,
style={[ {
styles.label_white, color: '#9ea2a8',
{ flexGrow: 1,
color: '#9ea2a8', paddingHorizontal: 0,
flexGrow: 1, marginBottom: 20
paddingHorizontal: 0, }
marginBottom: 20 ]}
} >
]} {this.state.type ? (
> 'Everyone can access this channel'
{this.state.type ? ( ) : (
'Everyone can access this channel' 'Just invited people can access this channel'
) : ( )}
'Just invited people can access this channel' </Text>
)} <TouchableOpacity
</Text> onPress={() => this.submit()}
<TouchableOpacity style={[
onPress={() => this.submit()} styles.buttonContainer_white,
style={[styles.buttonContainer_white, styles.enabledButton]} this.state.channelName.length === 0 || this.props.createChannel.isFetching
> ? styles.disabledButton
<Text style={styles.button_white}>CREATE</Text> : styles.enabledButton
</TouchableOpacity> ]}
</SafeAreaView> >
<Spinner <Text style={styles.button_white}>CREATE</Text>
visible={this.props.createChannel.isFetching} </TouchableOpacity>
textContent='Loading...' <Loading visible={this.props.createChannel.isFetching} />
textStyle={{ color: '#FFF' }} </SafeAreaView>
/> </ScrollView>
</KeyboardView> </KeyboardView>
); );
} }

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