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

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

View File

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

View File

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

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
* the correct one based on the CPU architecture of their device.
*/
def enableSeparateBuildPerCPUArchitecture = false
def enableSeparateBuildPerCPUArchitecture = true
/**
* Run Proguard to shrink the Java bytecode in release builds.
*/
def enableProguardInReleaseBuilds = false
def enableProguardInReleaseBuilds = true
android {
compileSdkVersion 25
@ -98,11 +98,12 @@ android {
minSdkVersion 16
targetSdkVersion 22
versionCode VERSIONCODE as Integer
versionName "1.1"
versionName "1"
ndk {
abiFilters "armeabi-v7a", "x86"
}
}
signingConfigs {
release {
if (project.hasProperty('KEYSTORE')) {
@ -123,9 +124,15 @@ android {
}
buildTypes {
release {
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
signingConfig signingConfigs.release
shrinkResources enableProguardInReleaseBuilds
zipAlignEnabled enableProguardInReleaseBuilds
minifyEnabled enableProguardInReleaseBuilds
useProguard enableProguardInReleaseBuilds
setProguardFiles([getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'])
}
debug {
applicationIdSuffix ".debug"
}
}
// applicationVariants are e.g. debug, release

View File

@ -18,7 +18,7 @@
# Disabling obfuscation is useful if you collect stack traces from production crashes
# (unless you are using a system that supports de-obfuscate the stack traces).
-dontobfuscate
# -dontobfuscate
# React Native
@ -49,6 +49,7 @@
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup <methods>; }
-dontwarn com.facebook.react.**
-keep,includedescriptorclasses class com.facebook.react.bridge.** { *; }
# TextLayoutBuilder uses a non-public Android constructor within StaticLayout.
# See libs/proxy/src/main/java/com/facebook/fbui/textlayoutbuilder/proxy for details.
@ -68,3 +69,25 @@
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
# Fresco
# Keep our interfaces so they can be used by other ProGuard rules.
# See http://sourceforge.net/p/proguard/bugs/466/
-keep,allowobfuscation @interface com.facebook.soloader.DoNotOptimize
# Do not strip any method/class that is annotated with @DoNotOptimize
-keep @com.facebook.soloader.DoNotOptimize class *
-keepclassmembers class * {
@com.facebook.soloader.DoNotOptimize *;
}
# Keep native methods
-keepclassmembers class * {
native <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:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:resizeableActivity="true"
android:largeHeap="true">
<activity
android:name=".MainActivity"
android:label="@string/app_name"

View File

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

View File

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

View File

@ -20,51 +20,3 @@ export function createChannelFailure(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';
export function openMentionedMessages(rid) {
export function openMentionedMessages(rid, limit) {
return {
type: types.MENTIONED_MESSAGES.OPEN,
rid
rid,
limit
};
}
export function readyMentionedMessages() {
return {
type: types.MENTIONED_MESSAGES.READY
};
}
export function closeMentionedMessages() {
return {
type: types.MENTIONED_MESSAGES.CLOSE

View File

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

View File

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

View File

@ -1,9 +1,16 @@
import * as types from './actionsTypes';
export function openRoomFiles(rid) {
export function openRoomFiles(rid, limit) {
return {
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';
export function openSnippetedMessages(rid) {
export function openSnippetedMessages(rid, limit) {
return {
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';
export function openStarredMessages(rid) {
export function openStarredMessages(rid, limit) {
return {
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 ESLINT_FIX = null;
export const COLOR_DANGER = '#f5455c';
export const COLOR_BUTTON_PRIMARY = '#2D6AEA';
export const COLOR_TEXT = '#292E35';
export const STATUS_COLORS = {
online: '#2de0a5',
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() {
const {
text = '', size = 25, baseUrl, borderRadius = 4, style, avatar, type = 'd'
text = '', size = 25, baseUrl, borderRadius = 2, style, avatar, type = 'd'
} = this.props;
const { initials, color } = avatarInitialsAndColor(`${ text }`);

View File

@ -6,11 +6,7 @@ import { connect } from 'react-redux';
const styles = StyleSheet.create({
bannerContainer: {
backgroundColor: '#ddd',
position: 'absolute',
top: '0%',
zIndex: 10,
width: '100%'
backgroundColor: '#ddd'
},
bannerText: {
textAlign: 'center',
@ -21,7 +17,8 @@ const styles = StyleSheet.create({
@connect(state => ({
connecting: state.meteor.connecting,
authenticating: state.login.isFetching,
offline: !state.meteor.connected
offline: !state.meteor.connected,
logged: !!state.login.token
}))
export default class Banner extends React.PureComponent {
@ -31,7 +28,9 @@ export default class Banner extends React.PureComponent {
offline: PropTypes.bool
}
render() {
const { connecting, authenticating, offline } = this.props;
const {
connecting, authenticating, offline, logged
} = this.props;
if (offline) {
return (
@ -40,6 +39,7 @@ export default class Banner extends React.PureComponent {
</View>
);
}
if (connecting) {
return (
<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 { ScrollView } from 'react-native';
import ScrollableTabView from 'react-native-scrollable-tab-view';
import _ from 'lodash';
import map from 'lodash/map';
import { emojify } from 'react-emojione';
import TabBar from './TabBar';
import EmojiCategory from './EmojiCategory';
@ -78,7 +78,7 @@ export default class EmojiPicker extends Component {
return emojiRow.length ? emojiRow[0].count + 1 : 1;
}
updateFrequentlyUsed() {
const frequentlyUsed = _.map(this.frequentlyUsed.slice(), (item) => {
const frequentlyUsed = map(this.frequentlyUsed.slice(), (item) => {
if (item.isCustom) {
return item;
}
@ -88,7 +88,7 @@ export default class EmojiPicker extends Component {
}
updateCustomEmojis() {
const customEmojis = _.map(this.customEmojis.slice(), item =>
const customEmojis = map(this.customEmojis.slice(), item =>
({ content: item.name, extension: item.extension, isCustom: true }));
this.setState({ customEmojis });
}

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

View File

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

View File

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

View File

@ -1,16 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StyleSheet, Text, Keyboard } from 'react-native';
import { View, StyleSheet, Text, Keyboard, LayoutAnimation } from 'react-native';
import { connect } from 'react-redux';
const styles = StyleSheet.create({
typing: {
transform: [{ scaleY: -1 }],
fontWeight: 'bold',
paddingHorizontal: 15,
height: 25
},
emptySpace: {
height: 5
}
});
@ -18,11 +19,13 @@ const styles = StyleSheet.create({
username: state.login.user && state.login.user.username,
usersTyping: state.room.usersTyping
}))
export default class Typing extends React.Component {
shouldComponentUpdate(nextProps) {
return this.props.usersTyping.join() !== nextProps.usersTyping.join();
}
componentWillUpdate() {
LayoutAnimation.easeInEaseOut();
}
onPress = () => {
Keyboard.dismiss();
}
@ -31,7 +34,13 @@ export default class Typing extends React.Component {
return users.length ? `${ users.join(' ,') } ${ users.length > 1 ? 'are' : 'is' } typing` : '';
}
render() {
return (<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 Icon from 'react-native-vector-icons/MaterialIcons';
import Slider from 'react-native-slider';
import { connect } from 'react-redux';
import Markdown from './Markdown';
const styles = StyleSheet.create({
audioContainer: {
flex: 1,
@ -61,6 +61,9 @@ const formatTime = (t = 0, duration = 0) => {
return `${ formattedMinutes }:${ formattedSeconds }`;
};
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}))
export default class Audio extends React.PureComponent {
static propTypes = {
file: PropTypes.object.isRequired,
@ -115,8 +118,8 @@ export default class Audio extends React.PureComponent {
const { uri, paused } = this.state;
const { description } = this.props.file;
return (
<View>
<View style={styles.audioContainer}>
[
<View key='audio' style={styles.audioContainer}>
<Video
ref={(ref) => {
this.player = ref;
@ -154,9 +157,9 @@ export default class Audio extends React.PureComponent {
onValueChange={value => this.setState({ currentTime: value })}
/>
</View>
</View>
<Markdown msg={description} />
</View>
</View>,
<Markdown key='description' msg={description} />
]
);
}
}

View File

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

View File

@ -1,41 +1,30 @@
import PropTypes from 'prop-types';
import React from 'react';
import { CachedImage } from 'react-native-img-cache';
import { Text, TouchableOpacity, View, StyleSheet } from 'react-native';
import { TouchableOpacity, StyleSheet } from 'react-native';
import { connect } from 'react-redux';
import PhotoModal from './PhotoModal';
import Markdown from './Markdown';
const styles = StyleSheet.create({
button: {
flex: 1,
flexDirection: 'column',
height: 320,
borderColor: '#ccc',
borderWidth: 1,
borderRadius: 6
flexDirection: 'column'
},
image: {
flex: 1,
height: undefined,
width: undefined,
resizeMode: 'contain'
width: 320,
height: 200,
resizeMode: 'cover'
},
labelContainer: {
height: 62,
alignItems: 'center',
justifyContent: 'center'
},
imageName: {
fontSize: 12,
alignSelf: 'center',
fontStyle: 'italic'
},
message: {
alignSelf: 'center',
fontWeight: 'bold'
alignItems: 'flex-start'
}
});
export default class Image extends React.PureComponent {
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}))
export default class extends React.PureComponent {
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
@ -45,8 +34,9 @@ export default class Image extends React.PureComponent {
state = { modalVisible: false };
getDescription() {
if (this.props.file.description) {
return <Text style={styles.message}>{this.props.file.description}</Text>;
const { file, customEmojis } = this.props;
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 img = `${ baseUrl }${ file.image_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
return (
<View>
[
<TouchableOpacity
key='image'
onPress={() => this._onPressButton()}
style={styles.button}
>
@ -69,18 +60,16 @@ export default class Image extends React.PureComponent {
style={styles.image}
source={{ uri: encodeURI(img) }}
/>
<View style={styles.labelContainer}>
<Text style={styles.imageName}>{this.props.file.title}</Text>
{this.getDescription()}
</View>
</TouchableOpacity>
</TouchableOpacity>,
<PhotoModal
key='modal'
title={this.props.file.title}
image={img}
isVisible={this.state.modalVisible}
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 SimpleMarkdown from 'simple-markdown';
import { emojify } from 'react-emojione';
import { connect } from 'react-redux';
import styles from './styles';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
@ -17,15 +18,7 @@ const BlockCode = ({ node, state }) => (
);
const mentionStyle = { color: '#13679a' };
const Markdown = ({
msg, customEmojis, style, markdownStyle, customRules, renderInline
}) => {
if (!msg) {
return null;
}
msg = emojify(msg, { output: 'unicode' });
const defaultRules = {
const defaultRules = {
username: {
order: -1,
match: SimpleMarkdown.inlineRegex(/^@[0-9a-zA-Z-_.]+/),
@ -96,7 +89,33 @@ const Markdown = ({
)
}
})
},
}
};
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-_.]+):/),
@ -119,24 +138,19 @@ const Markdown = ({
}
return element;
}
}
};
const codeStyle = StyleSheet.flatten(styles.codeStyle);
style = StyleSheet.flatten(style);
return (
<EasyMarkdown
style={{ marginBottom: 0, ...style }}
markdownStyles={{ code: codeStyle, ...markdownStyle }}
rules={{ ...defaultRules, ...customRules }}
},
...defaultRules,
...customRules
}}
renderInline={renderInline}
>{msg}
>{m}
</EasyMarkdown>
);
};
}
}
Markdown.propTypes = {
msg: PropTypes.string.isRequired,
msg: PropTypes.string,
customEmojis: PropTypes.object,
// eslint-disable-next-line react/no-typos
style: ViewPropTypes.style,
@ -149,5 +163,3 @@ BlockCode.propTypes = {
node: PropTypes.object,
state: PropTypes.object
};
export default Markdown;

View File

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

View File

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

View File

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

View File

@ -1,13 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, TouchableHighlight, Text, TouchableOpacity, Vibration, ViewPropTypes } from 'react-native';
import { View, Text, TouchableOpacity, Vibration, ViewPropTypes } from 'react-native';
import { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/MaterialIcons';
import moment from 'moment';
import equal from 'deep-equal';
import { KeyboardUtils } from 'react-native-keyboard-input';
import { actionsShow, errorActionsShow, toggleReactionPicker } from '../../actions/messages';
import Image from './Image';
import User from './User';
import Avatar from '../Avatar';
@ -18,13 +17,54 @@ import Url from './Url';
import Reply from './Reply';
import ReactionsModal from './ReactionsModal';
import Emoji from './Emoji';
import messageStatus from '../../constants/messagesStatus';
import styles from './styles';
import { actionsShow, errorActionsShow, toggleReactionPicker } from '../../actions/messages';
import messagesStatus from '../../constants/messagesStatus';
import Touch from '../../utils/touch';
const getInfoMessage = ({
t, role, msg, u
}) => {
if (t === 'rm') {
return 'Message removed';
} else if (t === 'uj') {
return 'Has joined the channel.';
} else if (t === 'r') {
return `Room name changed to: ${ msg } by ${ u.username }`;
} else if (t === 'message_pinned') {
return 'Message pinned';
} else if (t === 'ul') {
return 'Has left the channel.';
} else if (t === 'ru') {
return `User ${ msg } removed by ${ u.username }`;
} else if (t === 'au') {
return `User ${ msg } added by ${ u.username }`;
} else if (t === 'user-muted') {
return `User ${ msg } muted by ${ u.username }`;
} else if (t === 'user-unmuted') {
return `User ${ msg } unmuted by ${ u.username }`;
} else if (t === 'subscription-role-added') {
return `${ msg } was set ${ role } by ${ u.username }`;
} else if (t === 'subscription-role-removed') {
return `${ msg } is no longer ${ role } by ${ u.username }`;
} else if (t === 'room_changed_description') {
return `Room description changed to: ${ msg } by ${ u.username }`;
} else if (t === 'room_changed_announcement') {
return `Room announcement changed to: ${ msg } by ${ u.username }`;
} else if (t === 'room_changed_topic') {
return `Room topic changed to: ${ msg } by ${ u.username }`;
} else if (t === 'room_changed_privacy') {
return `Room type changed to: ${ msg } by ${ u.username }`;
}
return '';
};
@connect(state => ({
message: state.messages.message,
editing: state.messages.editing,
customEmojis: state.customEmojis
customEmojis: state.customEmojis,
Message_TimeFormat: state.settings.Message_TimeFormat,
Message_GroupingPeriod: state.settings.Message_GroupingPeriod
}), dispatch => ({
actionsShow: actionMessage => dispatch(actionsShow(actionMessage)),
errorActionsShow: actionMessage => dispatch(errorActionsShow(actionMessage)),
@ -35,13 +75,13 @@ export default class Message extends React.Component {
status: PropTypes.any,
item: PropTypes.object.isRequired,
reactions: PropTypes.any.isRequired,
baseUrl: PropTypes.string.isRequired,
Message_TimeFormat: PropTypes.string.isRequired,
Message_GroupingPeriod: PropTypes.number.isRequired,
customTimeFormat: PropTypes.string,
message: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
editing: PropTypes.bool,
errorActionsShow: PropTypes.func,
customEmojis: PropTypes.object,
toggleReactionPicker: PropTypes.func,
onReactionPress: PropTypes.func,
style: ViewPropTypes.style,
@ -63,28 +103,35 @@ export default class Message extends React.Component {
}
shouldComponentUpdate(nextProps, nextState) {
if (!equal(this.props.reactions, nextProps.reactions)) {
return true;
}
if (this.state.reactionsModal !== nextState.reactionsModal) {
return true;
}
return this.props._updatedAt.toGMTString() !== nextProps._updatedAt.toGMTString() || this.props.status !== nextProps.status;
if (this.props.status !== nextProps.status) {
return true;
}
// eslint-disable-next-line
if (!!this.props._updatedAt ^ !!nextProps._updatedAt) {
return true;
}
if (!equal(this.props.reactions, nextProps.reactions)) {
return true;
}
return this.props._updatedAt.toGMTString() !== nextProps._updatedAt.toGMTString();
}
onPress = () => {
KeyboardUtils.dismiss();
}
onLongPress() {
onLongPress = () => {
this.props.onLongPress(this.parseMessage());
}
onErrorPress() {
onErrorPress = () => {
this.props.errorActionsShow(this.parseMessage());
}
onReactionPress(emoji) {
onReactionPress = (emoji) => {
this.props.onReactionPress(emoji, this.props.item._id);
}
onClose() {
@ -95,45 +142,9 @@ export default class Message extends React.Component {
Vibration.vibrate(50);
}
getInfoMessage() {
let message = '';
const {
t, role, msg, u
} = this.props.item;
if (t === 'rm') {
message = 'Message removed';
} else if (t === 'uj') {
message = 'Has joined the channel.';
} else if (t === 'r') {
message = `Room name changed to: ${ msg } by ${ u.username }`;
} else if (t === 'message_pinned') {
message = 'Message pinned';
} else if (t === 'ul') {
message = 'Has left the channel.';
} else if (t === 'ru') {
message = `User ${ msg } removed by ${ u.username }`;
} else if (t === 'au') {
message = `User ${ msg } added by ${ u.username }`;
} else if (t === 'user-muted') {
message = `User ${ msg } muted by ${ u.username }`;
} else if (t === 'user-unmuted') {
message = `User ${ msg } unmuted by ${ u.username }`;
} else if (t === 'subscription-role-added') {
message = `${ msg } was set ${ role } by ${ u.username }`;
} else if (t === 'subscription-role-removed') {
message = `${ msg } is no longer ${ role } by ${ u.username }`;
} else if (t === 'room_changed_description') {
message = `Room description changed to: ${ msg } by ${ u.username }`;
} else if (t === 'room_changed_announcement') {
message = `Room announcement changed to: ${ msg } by ${ u.username }`;
} else if (t === 'room_changed_topic') {
message = `Room topic changed to: ${ msg } by ${ u.username }`;
} else if (t === 'room_changed_privacy') {
message = `Room type changed to: ${ msg } by ${ u.username }`;
}
return message;
get timeFormat() {
const { customTimeFormat, Message_TimeFormat } = this.props;
return customTimeFormat || Message_TimeFormat;
}
parseMessage = () => JSON.parse(JSON.stringify(this.props.item));
@ -163,64 +174,97 @@ export default class Message extends React.Component {
}
isTemp() {
return this.props.item.status === messageStatus.TEMP || this.props.item.status === messageStatus.ERROR;
return this.props.item.status === messagesStatus.TEMP || this.props.item.status === messagesStatus.ERROR;
}
hasError() {
return this.props.item.status === messageStatus.ERROR;
return this.props.item.status === messagesStatus.ERROR;
}
attachments() {
renderHeader = (username) => {
const { item, previousItem } = this.props;
if (previousItem && (
(previousItem.ts.toDateString() === item.ts.toDateString()) &&
(previousItem.u.username === item.u.username) &&
!(previousItem.groupable === false || item.groupable === false) &&
(previousItem.status === item.status) &&
(item.ts - previousItem.ts < this.props.Message_GroupingPeriod * 1000)
)) {
return null;
}
return (
<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) {
return null;
}
const file = this.props.item.attachments[0];
const { baseUrl, user } = this.props;
const { user } = this.props;
if (file.image_type) {
return <Image file={file} baseUrl={baseUrl} user={user} />;
} else if (file.audio_type) {
return <Audio file={file} baseUrl={baseUrl} user={user} />;
} else if (file.video_type) {
return <Video file={file} baseUrl={baseUrl} user={user} />;
return <Image file={file} user={user} />;
}
if (file.audio_type) {
return <Audio file={file} 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() {
if (this.isInfoMessage()) {
return <Text style={styles.textInfo}>{this.getInfoMessage()}</Text>;
}
const { item, customEmojis, baseUrl } = this.props;
return <Markdown msg={item.msg} customEmojis={customEmojis} baseUrl={baseUrl} />;
}
renderUrl() {
if (this.props.item.urls.length === 0) {
renderUrl = () => {
const { urls } = this.props.item;
if (urls.length === 0) {
return null;
}
return this.props.item.urls.map(url => (
return urls.map(url => (
<Url url={url} key={url.url} />
));
}
};
renderError = () => {
if (!this.hasError()) {
return null;
}
return (
<TouchableOpacity onPress={() => this.onErrorPress()}>
<Icon name='error-outline' color='red' size={20} style={{ padding: 10, paddingRight: 12, paddingLeft: 0 }} />
<TouchableOpacity onPress={this.onErrorPress}>
<Icon name='error-outline' color='red' size={20} style={styles.errorIcon} />
</TouchableOpacity>
);
}
renderReaction(reaction) {
renderReaction = (reaction) => {
const reacted = reaction.usernames.findIndex(item => item.value === this.props.user.username) !== -1;
const reactedContainerStyle = reacted ? { borderColor: '#bde1fe', backgroundColor: '#f3f9ff' } : {};
const reactedCount = reacted ? { color: '#4fb0fc' } : {};
const reactedContainerStyle = reacted && styles.reactedContainer;
const reactedCount = reacted && styles.reactedCountText;
return (
<TouchableOpacity
onPress={() => this.onReactionPress(reaction.emoji)}
@ -232,7 +276,6 @@ export default class Message extends React.Component {
content={reaction.emoji}
standardEmojiStyle={styles.reactionEmoji}
customEmojiStyle={styles.reactionCustomEmoji}
customEmojis={this.props.customEmojis}
/>
<Text style={[styles.reactionCount, reactedCount]}>{ reaction.usernames.length }</Text>
</View>
@ -246,7 +289,7 @@ export default class Message extends React.Component {
}
return (
<View style={styles.reactionsContainer}>
{this.props.item.reactions.map(reaction => this.renderReaction(reaction))}
{this.props.item.reactions.map(this.renderReaction)}
<TouchableOpacity
onPress={() => this.props.toggleReactionPicker(this.parseMessage())}
key='add-reaction'
@ -260,57 +303,42 @@ export default class Message extends React.Component {
render() {
const {
item, message, editing, baseUrl, customEmojis, style, archived
item, message, editing, style, archived
} = this.props;
const username = item.alias || item.u.username;
const isEditing = message._id === item._id && editing;
const accessibilityLabel = `Message from ${ username } at ${ moment(item.ts).format(this.props.Message_TimeFormat) }, ${ this.props.item.msg }`;
const accessibilityLabel = `Message from ${ username } at ${ moment(item.ts).format(this.timeFormat) }, ${ this.props.item.msg }`;
return (
<TouchableHighlight
onPress={() => this.onPress()}
onLongPress={() => this.onLongPress()}
disabled={this.isDeleted() || this.hasError() || archived}
<Touch
onPress={this.onPress}
onLongPress={this.onLongPress}
disabled={this.isInfoMessage() || this.hasError() || archived}
underlayColor='#FFFFFF'
activeOpacity={0.3}
style={[styles.message, isEditing ? styles.editing : null, style]}
accessibilityLabel={accessibilityLabel}
>
<View style={[styles.message, isEditing && styles.editing, style]}>
{this.renderHeader(username)}
<View style={styles.flex}>
{this.renderError()}
<View style={[this.isTemp() && { opacity: 0.3 }, styles.flex]}>
<Avatar
style={styles.avatar}
text={item.avatar ? '' : username}
size={40}
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()}
<View style={[styles.messageContent, this.isTemp() && styles.temp]}>
{this.renderContent()}
{this.renderAttachment()}
{this.renderUrl()}
{this.renderReactions()}
</View>
</View>
{this.state.reactionsModal ?
{this.state.reactionsModal &&
<ReactionsModal
isVisible={this.state.reactionsModal}
onClose={this.onClose}
reactions={item.reactions}
user={this.props.user}
customEmojis={customEmojis}
/>
: null
}
</View>
</TouchableHighlight>
</Touch>
);
}
}

View File

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

View File

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

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 } })
]
});
config.navigator.dispatch(action);
}
export function dispatch(action) {
if (config.navigator) {
config.navigator.dispatch(action);
}
}

View File

@ -5,15 +5,21 @@ import Icon from 'react-native-vector-icons/FontAwesome';
import ListServerView from '../../views/ListServerView';
import NewServerView from '../../views/NewServerView';
import LoginSignupView from '../../views/LoginSignupView';
import LoginView from '../../views/LoginView';
import RegisterView from '../../views/RegisterView';
import TermsServiceView from '../../views/TermsServiceView';
import PrivacyPolicyView from '../../views/PrivacyPolicyView';
import ForgotPasswordView from '../../views/ForgotPasswordView';
import database from '../../lib/realm';
const PublicRoutes = StackNavigator(
{
const hasServers = () => {
const db = database.databases.serversDB.objects('servers');
return db.length > 0;
};
const ServerStack = StackNavigator({
ListServer: {
screen: ListServerView,
navigationOptions({ navigation }) {
@ -35,44 +41,78 @@ const PublicRoutes = StackNavigator(
AddServer: {
screen: NewServerView,
navigationOptions: {
title: 'New server'
header: null
}
},
LoginSignup: {
screen: LoginSignupView,
navigationOptions: {
header: null
}
}
}, {
headerMode: 'screen',
initialRouteName: hasServers() ? 'ListServer' : 'AddServer'
});
const LoginStack = StackNavigator({
Login: {
screen: LoginView,
navigationOptions: {
title: 'Login'
}
},
Register: {
screen: RegisterView,
navigationOptions: {
title: 'Register'
}
},
TermsService: {
screen: TermsServiceView,
navigationOptions: {
title: 'Terms of service'
}
},
PrivacyPolicy: {
screen: PrivacyPolicyView,
navigationOptions: {
title: 'Privacy policy'
header: null
}
},
ForgotPassword: {
screen: ForgotPasswordView,
navigationOptions: {
title: 'Forgot my password'
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(
{
Server: {
screen: ServerStack
},
Login: {
screen: LoginStack
},
Register: {
screen: RegisterStack
}
},
{
navigationOptions: {
headerTitleAllowFontScaling: false
}
mode: 'modal',
headerMode: 'none'
}
);

View File

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

View File

@ -1,4 +1,23 @@
import EJSON from 'ejson';
import { Answers } from 'react-native-fabric';
import { AppState } from 'react-native';
import debounce from '../utils/debounce';
// import { AppState, NativeModules } from 'react-native';
// const { WebSocketModule, BlobManager } = NativeModules;
// class WS extends WebSocket {
// _close(code?: number, reason?: string): void {
// if (Platform.OS === 'android') {
// WebSocketModule.close(code, reason, this._socketId);
// } else {
// WebSocketModule.close(this._socketId);
// }
//
// if (BlobManager.isAvailable && this._binaryType === 'blob') {
// BlobManager.removeWebSocketHandler(this._socketId);
// }
// }
// }
class EventEmitter {
constructor() {
@ -9,6 +28,7 @@ class EventEmitter {
this.events[event] = [];
}
this.events[event].push(listener);
return listener;
}
removeListener(event, listener) {
if (typeof this.events[event] === 'object') {
@ -24,7 +44,8 @@ class EventEmitter {
try {
listener.apply(this, args);
} catch (e) {
console.log(e);
Answers.logCustom(e);
console.warn(e);
}
});
}
@ -34,72 +55,195 @@ class EventEmitter {
this.removeListener(event, g);
listener.apply(this, args);
});
return listener;
}
}
export default class Socket extends EventEmitter {
constructor(url) {
constructor(url, login) {
super();
this.url = url.replace(/^http/, 'ws');
this.state = 'active';
this.lastping = new Date();
this._login = login;
this.url = url;// .replace(/^http/, 'ws');
this.id = 0;
this.subscriptions = {};
this._connect();
this.ddp = new EventEmitter();
this.on('ping', () => this.send({ msg: 'pong' }));
this._logged = false;
const waitTimeout = () => setTimeout(async() => {
// this.connection.ping();
this.send({ msg: 'ping' });
this.timeout = setTimeout(() => this.reconnect(), 1000);
}, 40000);
const handlePing = () => {
this.lastping = new Date();
this.send({ msg: 'pong' }, true);
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = waitTimeout();
};
const handlePong = () => {
this.lastping = new Date();
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = waitTimeout();
};
AppState.addEventListener('change', (nextAppState) => {
if (this.state && this.state.match(/inactive/) && nextAppState === 'active') {
try {
this.send({ msg: 'ping' }, true);
// this.connection.ping();
} catch (e) {
this.reconnect();
}
}
if (this.state && this.state.match(/background/) && nextAppState === 'active') {
this.emit('background');
}
this.state = nextAppState;
});
this.on('pong', handlePong);
this.on('ping', handlePing);
this.on('result', data => this.ddp.emit(data.id, { id: data.id, result: data.result, error: data.error }));
this.on('ready', data => this.ddp.emit(data.subs[0], data));
// this.on('error', () => this.reconnect());
this.on('disconnected', debounce(() => this.reconnect(), 300));
this.on('logged', () => this._logged = true);
this.on('logged', () => {
Object.keys(this.subscriptions || {}).forEach((key) => {
const { name, params } = this.subscriptions[key];
this.subscriptions[key].unsubscribe();
this.subscribe(name, ...params);
});
});
this.on('open', async() => {
this._logged = false;
this.send({ msg: 'connect', version: '1', support: ['1', 'pre2', 'pre1'] });
});
this._connect();
}
send(obj) {
check() {
if (!this.lastping) {
return false;
}
if ((Math.abs(this.lastping.getTime() - new Date().getTime()) / 1000) > 50) {
return false;
}
return true;
}
async login(params) {
try {
this.emit('login', params);
const result = await this.call('login', params);
this._login = { resume: result.token, ...result };
this._logged = true;
this.emit('logged', result);
return result;
} catch (err) {
const error = { ...err };
if (/user not found/i.test(error.reason)) {
error.error = 1;
error.reason = 'User or Password incorrect';
error.message = 'User or Password incorrect';
}
this.emit('logginError', error);
return Promise.reject(error);
}
}
async send(obj, ignore) {
console.log('send');
return new Promise((resolve, reject) => {
this.id += 1;
const id = obj.id || `${ this.id }`;
const id = obj.id || `ddp-react-native-${ this.id }`;
// console.log('send', { ...obj, id });
this.connection.send(EJSON.stringify({ ...obj, id }));
this.ddp.once(id, data => (data.error ? reject(data.error) : resolve({ id, ...data })));
if (ignore) {
return;
}
const cancel = this.ddp.once('disconnected', reject);
this.ddp.once(id, (data) => {
// console.log(data);
this.ddp.removeListener(id, cancel);
return (data.error ? reject(data.error) : resolve({ id, ...data }));
});
});
}
get status() {
return this.connection && this.connection.readyState === 1 && this.check() && !!this._logged;
}
_close() {
try {
// this.connection && this.connection.readyState > 1 && this.connection.close && this.connection.close(300, 'disconnect');
if (this.connection && this.connection.close) {
this.connection.close(300, 'disconnect');
delete this.connection;
}
} catch (e) {
// console.log(e);
}
}
_connect() {
const connection = new WebSocket(`${ this.url }/websocket`);
connection.onopen = () => {
this.emit('open');
this.send({ msg: 'connect', version: '1', support: ['1', 'pre2', 'pre1'] });
};
connection.onclose = e => this.emit('disconnected', e);
// connection.onerror = () => {
// // alert(error.type);
// // console.log(error);
// // console.log(`WebSocket Error ${ JSON.stringify({...error}) }`);
// };
return new Promise((resolve) => {
this.lastping = new Date();
this._close();
clearInterval(this.reconnect_timeout);
this.reconnect_timeout = setInterval(() => (!this.connection || this.connection.readyState > 1 || !this.check()) && this.reconnect(), 5000);
this.connection = new WebSocket(`${ this.url }/websocket`, null);
connection.onmessage = (e) => {
this.connection.onopen = () => {
this.emit('open');
resolve();
this.ddp.emit('open');
return this._login && this.login(this._login);
};
this.connection.onclose = debounce((e) => { console.log('aer'); this.emit('disconnected', e); }, 300);
this.connection.onmessage = (e) => {
try {
// console.log('received', e.data, e.target.readyState);
const data = EJSON.parse(e.data);
this.emit(data.msg, data);
return data.collection && this.emit(data.collection, data);
} catch (err) {
Answers.logCustom('EJSON parse', err);
}
};
// this.on('disconnected', e => alert(JSON.stringify(e)));
this.connection = connection;
});
}
logout() {
this._login = null;
return this.call('logout').then(() => this.subscriptions = {});
}
disconnect() {
this.emit('disconnected_by_user');
this.connection.close();
this._close();
}
reconnect() {
this.disconnect();
this.once('connected', () => {
Object.keys(this.subscriptions).forEach((key) => {
const { name, params } = this.subscriptions[key];
this.subscriptions[key].unsubscribe();
this.subscribe(name, params);
});
});
async reconnect() {
if (this._timer) {
return;
}
delete this.connection;
this._logged = false;
this._timer = setTimeout(() => {
delete this._timer;
this._connect();
}, 1000);
}
call(method, ...params) {
return this.send({
msg: 'method', method, params
}).then(data => data.result || data.subs);
}).then(data => data.result || data.subs).catch((err) => {
Answers.logCustom('DDP call Error', err);
return Promise.reject(err);
});
}
unsubscribe(id) {
if (!this.subscriptions[id]) {
@ -109,19 +253,31 @@ export default class Socket extends EventEmitter {
return this.send({
msg: 'unsub',
id
}).then(data => data.result || data.subs);
}).then(data => data.result || data.subs).catch((err) => {
console.warn('unsubscribe', err);
Answers.logCustom('DDP unsubscribe Error', err);
return Promise.reject(err);
});
}
subscribe(name, ...params) {
console.log(name, params);
return this.send({
msg: 'sub', name, params
}).then(({ id }) => {
const args = {
id,
name,
params,
unsubscribe: () => this.unsubscribe(id)
};
this.subscriptions[id] = args;
// console.log(args);
return args;
}).catch((err) => {
console.warn('subscribe', err);
Answers.logCustom('DDP subscribe Error', err);
return Promise.reject(err);
});
}
}

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',
t: 'string',
lastMessage: 'messages',
description: { type: 'string', optional: true },
_updatedAt: { type: 'date', optional: true }
}
};
@ -63,6 +64,14 @@ const subscriptionRolesSchema = {
}
};
const userMutedInRoomSchema = {
name: 'usersMuted',
primaryKey: 'value',
properties: {
value: 'string'
}
};
const subscriptionSchema = {
name: 'subscriptions',
primaryKey: '_id',
@ -90,7 +99,9 @@ const subscriptionSchema = {
blocked: { type: 'bool', optional: true },
reactWhenReadOnly: { type: 'bool', optional: true },
archived: { type: 'bool', optional: true },
joinCodeRequired: { type: 'bool', optional: true }
joinCodeRequired: { type: 'bool', optional: true },
notifications: { type: 'bool', optional: true },
muted: { type: 'list', objectType: 'usersMuted' }
}
};
@ -137,7 +148,9 @@ const attachment = {
color: { type: 'string', optional: true },
ts: { type: 'date', optional: true },
attachments: { type: 'list', objectType: 'attachment' },
fields: { type: 'list', objectType: 'attachmentFields' }
fields: {
type: 'list', objectType: 'attachmentFields', default: []
}
}
};
@ -265,8 +278,48 @@ const schema = [
customEmojisSchema,
messagesReactionsSchema,
messagesReactionsUsernamesSchema,
rolesSchema
rolesSchema,
userMutedInRoomSchema
];
// class DebouncedDb {
// constructor(db) {
// this.database = db;
// }
// deleteAll(...args) {
// return this.database.write(() => this.database.deleteAll(...args));
// }
// delete(...args) {
// return this.database.delete(...args);
// }
// write(fn) {
// return fn();
// }
// create(...args) {
// this.queue = this.queue || [];
// if (this.timer) {
// clearTimeout(this.timer);
// this.timer = null;
// }
// this.timer = setTimeout(() => {
// alert(this.queue.length);
// this.database.write(() => {
// this.queue.forEach(({ db, args }) => this.database.create(...args));
// });
//
// this.timer = null;
// return this.roles = [];
// }, 1000);
//
// this.queue.push({
// db: this.database,
// args
// });
// }
// objects(...args) {
// return this.database.objects(...args);
// }
// }
class DB {
databases = {
serversDB: new Realm({
@ -296,7 +349,7 @@ class DB {
return this.databases.activeDB;
}
setActiveDB(database) {
setActiveDB(database = '') {
const path = database.replace(/(^\w+:|^)\/\//, '');
return this.databases.activeDB = new Realm({
path: `${ path }.realm`,

View File

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

View File

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

View File

@ -3,7 +3,8 @@ import { CREATE_CHANNEL } from '../actions/actionsTypes';
const initialState = {
isFetching: false,
failure: false,
users: []
result: '',
error: ''
};
export default function messages(state = initialState, action) {
@ -11,9 +12,9 @@ export default function messages(state = initialState, action) {
case CREATE_CHANNEL.REQUEST:
return {
...state,
error: undefined,
isFetching: true,
failure: false,
isFetching: true
error: ''
};
case CREATE_CHANNEL.SUCCESS:
return {
@ -29,18 +30,6 @@ export default function messages(state = initialState, action) {
failure: true,
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:
return state;
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -28,4 +28,5 @@ const handleRequest = function* handleRequest({ data }) {
const root = function* root() {
yield takeLatest(CREATE_CHANNEL.REQUEST, handleRequest);
};
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 hello from './hello';
import login from './login';
import connect from './connect';
import rooms from './rooms';
@ -18,7 +17,6 @@ const root = function* root() {
yield all([
init(),
createChannel(),
hello(),
rooms(),
login(),
connect(),

View File

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

View File

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

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

View File

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

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

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

View File

@ -1,9 +1,9 @@
import { Alert } from 'react-native';
import { put, call, takeLatest, take, select, race, fork, cancel, takeEvery } from 'redux-saga/effects';
import { delay } from 'redux-saga';
import { FOREGROUND, BACKGROUND } from 'redux-enhancer-react-native-appstate';
import { BACKGROUND } from 'redux-enhancer-react-native-appstate';
import * as types from '../actions/actionsTypes';
import { roomsSuccess, roomsFailure } from '../actions/rooms';
// import { roomsSuccess, roomsFailure } from '../actions/rooms';
import { addUserTyping, removeUserTyping, setLastOpen } from '../actions/room';
import { messagesRequest } from '../actions/messages';
import RocketChat from '../lib/rocketchat';
@ -13,18 +13,18 @@ import * as NavigationService from '../containers/routes/NavigationService';
const leaveRoom = rid => RocketChat.leaveRoom(rid);
const eraseRoom = rid => RocketChat.eraseRoom(rid);
const getRooms = function* getRooms() {
return yield RocketChat.getRooms();
};
// const getRooms = function* getRooms() {
// return yield RocketChat.getRooms();
// };
const watchRoomsRequest = function* watchRoomsRequest() {
try {
yield call(getRooms);
yield put(roomsSuccess());
} catch (err) {
yield put(roomsFailure(err.status));
}
};
// const watchRoomsRequest = function* watchRoomsRequest() {
// try {
// yield call(getRooms);
// yield put(roomsSuccess());
// } catch (err) {
// yield put(roomsFailure(err.status));
// }
// };
const cancelTyping = function* cancelTyping(username) {
while (true) {
@ -50,6 +50,7 @@ const usersTyping = function* usersTyping({ rid }) {
}
};
const handleMessageReceived = function* handleMessageReceived({ message }) {
try {
const room = yield select(state => state.room);
if (message.rid === room.rid) {
@ -59,36 +60,36 @@ const handleMessageReceived = function* handleMessageReceived({ message }) {
RocketChat.readMessages(room.rid);
}
} catch (e) {
console.warn('handleMessageReceived', e);
}
};
const watchRoomOpen = function* watchRoomOpen({ room }) {
const auth = yield select(state => state.login.isAuthenticated);
if (!auth) {
yield take(types.LOGIN.SUCCESS);
}
yield put(messagesRequest({ ...room }));
// const { open } = yield race({
// messages: take(types.MESSAGES.SUCCESS),
// open: take(types.ROOM.OPEN)
// });
//
// if (open) {
// return;
// }
yield put(messagesRequest({ rid: room.rid }));
const { open } = yield race({
messages: take(types.MESSAGES.SUCCESS),
open: take(types.ROOM.OPEN)
});
if (open) {
return;
}
RocketChat.readMessages(room.rid);
const subscriptions = yield Promise.all([RocketChat.subscribe('stream-room-messages', room.rid, false), RocketChat.subscribe('stream-notify-room', `${ room.rid }/typing`, false)]);
const sub = yield RocketChat.subscribeRoom(room);
// const subscriptions = yield Promise.all([RocketChat.subscribe('stream-room-messages', room.rid, false), RocketChat.subscribe('stream-notify-room', `${ room.rid }/typing`, false)]);
const thread = yield fork(usersTyping, { rid: room.rid });
yield race({
open: take(types.ROOM.OPEN),
close: take(types.ROOM.CLOSE)
});
cancel(thread);
subscriptions.forEach((sub) => {
sub.unsubscribe().catch(e => alert(e));
});
sub.stop();
// subscriptions.forEach((sub) => {
// sub.unsubscribe().catch(e => alert(e));
// });
};
const watchuserTyping = function* watchuserTyping({ status }) {
@ -110,13 +111,13 @@ const watchuserTyping = function* watchuserTyping({ status }) {
}
};
const updateRoom = function* updateRoom() {
const room = yield select(state => state.room);
if (!room || !room.rid) {
return;
}
yield put(messagesRequest({ rid: room.rid }));
};
// const updateRoom = function* updateRoom() {
// const room = yield select(state => state.room);
// if (!room || !room.rid) {
// return;
// }
// yield put(messagesRequest({ rid: room.rid }));
// };
const updateLastOpen = function* updateLastOpen() {
yield put(setLastOpen());
@ -157,11 +158,10 @@ const handleEraseRoom = function* handleEraseRoom({ rid }) {
const root = function* root() {
yield takeLatest(types.ROOM.USER_TYPING, watchuserTyping);
yield takeLatest(types.LOGIN.SUCCESS, watchRoomsRequest);
yield takeLatest(types.ROOM.OPEN, watchRoomOpen);
yield takeEvery(types.ROOM.MESSAGE_RECEIVED, handleMessageReceived);
yield takeLatest(FOREGROUND, updateRoom);
yield takeLatest(FOREGROUND, watchRoomsRequest);
// yield takeLatest(FOREGROUND, updateRoom);
// yield takeLatest(FOREGROUND, watchRoomsRequest);
yield takeLatest(BACKGROUND, updateLastOpen);
yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom);
yield takeLatest(types.ROOM.ERASE, handleEraseRoom);

View File

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

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

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

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

View File

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

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