Sync develop on master (#275)

* Create LICENSE
This commit is contained in:
Guilherme Gazzo 2018-04-21 16:10:44 -03:00 committed by GitHub
parent 2c73857186
commit 302df46d69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
272 changed files with 25087 additions and 17358 deletions

View File

@ -1,4 +1,9 @@
{
"presets": ["react-native"],
"plugins": ["transform-decorators-legacy"]
"plugins": ["transform-decorators-legacy"],
"env": {
"production": {
"plugins": ["transform-remove-console"]
}
}
}

7
.circleci/changelog.sh Normal file
View File

@ -0,0 +1,7 @@
#!/bin/bash
git log --format="%cd" -n 14 --date=short | sort -u -r | while read DATE ; do
echo $DATE
GIT_PAGER=cat git log --no-merges --format="- %s" --since="$DATE 00:00:00" --until="$DATE 24:00:00"
echo
done

238
.circleci/config.yml Normal file
View File

@ -0,0 +1,238 @@
defaults: &defaults
working_directory: ~/repo
version: 2
jobs:
lint-testunit:
<<: *defaults
docker:
- image: circleci/node:8
environment:
CODECOV_TOKEN: caa771ab-3d45-4756-8e2a-e1f25996fef6
steps:
- checkout
- run:
name: Install NPM modules
command: |
npm install
# npm install codecov
- run:
name: Lint
command: |
npm run lint
- run:
name: Test
command: |
npm test
- run:
name: Codecov
command: |
npx codecov
android-build:
<<: *defaults
docker:
- image: circleci/android:api-26-alpha
environment:
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"
JVM_OPTS: -Xmx4096m
TERM: dumb
BASH_ENV: "~/.nvm/nvm.sh"
steps:
- checkout
- run:
name: Install Node 8
command: |
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
source ~/.nvm/nvm.sh
nvm install 8
- restore_cache:
key: node-modules-{{ checksum ".circleci/config.yml" }}-{{ checksum "package.json" }}
- run:
name: Install NPM modules
command: |
npm install
- restore_cache:
key: android-{{ checksum ".circleci/config.yml" }}-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
- run:
name: Configure Gradle
command: |
cd android
echo -e "" > ./gradle.properties
if [[ $KEYSTORE ]]; then
echo $KEYSTORE_BASE64 | base64 --decode > ./app/$KEYSTORE
echo -e "KEYSTORE=$KEYSTORE" >> ./gradle.properties
echo -e "KEYSTORE_PASSWORD=$KEYSTORE_PASSWORD" >> ./gradle.properties
echo -e "KEY_ALIAS=$KEY_ALIAS" >> ./gradle.properties
echo -e "KEY_PASSWORD=$KEYSTORE_PASSWORD" >> ./gradle.properties
fi
echo -e "VERSIONCODE=$CIRCLE_BUILD_NUM" >> ./gradle.properties
echo -e "" > ./app/fabric.properties
echo -e "apiKey=$FABRIC_KEY" >> ./app/fabric.properties
echo -e "apiSecret=$FABRIC_SECRET" >> ./app/fabric.properties
- run:
name: Install Android Depedencies
command: |
cd android
./gradlew androidDependencies
- run:
name: Build Android App
command: |
cd android
if [[ $KEYSTORE ]]; then
./gradlew assembleRelease
else
./gradlew assembleDebug
fi
mkdir -p /tmp/build
mv app/build/outputs /tmp/build/
- store_artifacts:
path: /tmp/build/outputs
- save_cache:
key: node-modules-{{ checksum ".circleci/config.yml" }}-{{ checksum "package.json" }}
paths:
- ./node_modules
- save_cache:
key: android-{{ checksum ".circleci/config.yml" }}-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
paths:
- ~/.gradle
ios-build:
macos:
xcode: "9.0"
environment:
BASH_ENV: "~/.nvm/nvm.sh"
steps:
- checkout
- run:
name: Install Node 8
command: |
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash
source ~/.nvm/nvm.sh
# https://github.com/creationix/nvm/issues/1394
set +e
nvm install 8
- run:
name: Update Fastlane
command: |
brew update
brew install ruby
sudo gem install fastlane
- run:
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
command: |
# Fix error https://github.com/facebook/react-native/issues/14382
cd node_modules/react-native/scripts/
curl https://raw.githubusercontent.com/facebook/react-native/5c53f89dd86160301feee024bce4ce0c89e8c187/scripts/ios-configure-glog.sh > ios-configure-glog.sh
chmod +x ios-configure-glog.sh
- run:
name: Fastlane Build
no_output_timeout: 1200
command: |
cd ios
agvtool new-version -all $CIRCLE_BUILD_NUM
/usr/libexec/PlistBuddy -c "Set Fabric:APIKey $FABRIC_KEY" ./RocketChatRN/Info.plist
echo -e "./Fabric.framework/run $FABRIC_KEY $FABRIC_SECRET" > ./RocketChatRN/Fabric.sh
if [[ $MATCH_KEYCHAIN_NAME ]]; then
fastlane ios release
else
export MATCH_KEYCHAIN_NAME="temp"
export MATCH_KEYCHAIN_PASSWORD="temp"
fastlane ios build
fi
- store_artifacts:
path: ios/RocketChatRN.ipa
- persist_to_workspace:
root: .
paths:
- ios/*.ipa
- ios/fastlane/report.xml
ios-testflight:
macos:
xcode: "9.0"
steps:
- checkout
- attach_workspace:
at: ios
- run:
name: Update Fastlane
command: |
brew update
brew install ruby
sudo gem install fastlane
- run:
name: Fastlane Tesflight Upload
command: |
cd ios
fastlane pilot upload --ipa ios/RocketChatRN.ipa --changelog "$(sh ../.circleci/changelog.sh)"
workflows:
version: 2
build-and-test:
jobs:
- lint-testunit
- ios-build:
requires:
- lint-testunit
- ios-testflight:
requires:
- ios-build
filters:
branches:
only:
- develop
- master
# - ios-testflight:
# requires:
# - ios-hold-testflight
- android-build:
requires:
- lint-testunit

View File

@ -1,4 +1,11 @@
{
module.exports = {
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".ios.js", ".android.js"]
}
}
},
"parser": "babel-eslint",
"extends": "airbnb",
"parserOptions": {
@ -30,6 +37,7 @@
"react/no-unused-prop-types": [2, {
"skipShapeProps": true
}],
"react/no-did-mount-set-state": 0,
"react/no-multi-comp": [0],
"react/jsx-indent": [2, "tab"],
"react/jsx-indent-props": [2, "tab"],
@ -37,6 +45,7 @@
"jsx-quotes": [2, "prefer-single"],
"jsx-a11y/href-no-hash": 0,
"import/prefer-default-export": 0,
"camelcase": 0,
"no-underscore-dangle": 0,
"no-return-assign": 0,
"no-param-reassign": 0,
@ -115,9 +124,10 @@
"prefer-const": 2,
"object-shorthand": 2,
"consistent-return": 0,
"global-require": "off"
"global-require": "off",
"react/prop-types": [0, { skipUndeclared: true }]
},
"globals": {
"__DEV__": true
}
}
};

19
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,19 @@
Before writing an issue, please make sure you're talking about the native application and not the Cordova one. If you are looking to open an issue to the Cordova application, go to this URL: https://github.com/RocketChat/Rocket.Chat.Cordova.
- Your Rocket.Chat.Android app version: ####
<!-- Make sure you are running the latest version (which can be found on the hostname screen or by opening the side menu and then clicking on the chevron alongside username -->
- Your Rocket.Chat.iOS app version: ####
<!-- Make sure you are running the latest version (which can be found on the hostname screen or by opening the side menu and then clicking on the chevron alongside username -->
- Your Rocket.Chat server version: ####
- Device model (or emulator) you're running with: ####
<!-- e.g. For android : Nexus 7 - Android 6.0.1 -->
<!-- e.g. For iOS : iPhone 6 - iOS 11.2
- Steps to reproduce:
<!-- Stack traces may help too. -->
**The app isn't connecting to your server?**
Make sure your server supports WebSocket. These are the minimum requirements for Apache 2.4 and Nginx 1.3 or greater.

7
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,7 @@
<!-- INSTRUCTION: Keep the line below to notify all core developers about this new PR -->
@RocketChat/ReactNative
<!-- INSTRUCTION: Inform the issue number that this PR closes, or remove the line below -->
Closes #ISSUE_NUMBER
<!-- INSTRUCTION: Tell us more about your PR with screen shots if you can -->

3
.gitignore vendored
View File

@ -28,6 +28,7 @@ build/
.idea
.gradle
local.properties
fabric.properties
*.iml
# node.js
@ -55,3 +56,5 @@ fastlane/Preview.html
fastlane/screenshots
coverage
.vscode/

26
.snyk Normal file
View File

@ -0,0 +1,26 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.7.1
ignore: {}
# patches apply the minimum changes required to fix a vulnerability
patch:
'npm:debug:20170905':
- react-native > connect > debug:
patched: '2017-09-29T23:29:20.238Z'
- react-native > connect > express-session > debug:
patched: '2017-09-29T23:29:20.238Z'
- react-native > connect > finalhandler > debug:
patched: '2017-09-29T23:29:20.238Z'
- react-native > connect > morgan > debug:
patched: '2017-09-29T23:29:20.238Z'
- react-native > connect > serve-index > debug:
patched: '2017-09-29T23:29:20.238Z'
- react-native > connect > body-parser > debug:
patched: '2017-09-29T23:29:20.238Z'
- react-native > connect > compression > debug:
patched: '2017-09-29T23:29:20.238Z'
- react-native > connect > connect-timeout > debug:
patched: '2017-09-29T23:29:20.238Z'
- react-native > connect > serve-static > send > debug:
patched: '2017-09-29T23:29:20.238Z'
- realm > extract-zip > debug:
patched: '2017-09-29T23:29:20.238Z'

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015-2018 Rocket.Chat Technologies Corp.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,5 +1,7 @@
# Rocket.Chat React Native Mobile
[![Greenkeeper badge](https://badges.greenkeeper.io/RocketChat/Rocket.Chat.ReactNative.svg)](https://greenkeeper.io/)
[![Build Status](https://img.shields.io/travis/RocketChat/Rocket.Chat.ReactNative/master.svg)](https://travis-ci.org/RocketChat/Rocket.Chat.ReactNative)
[![Project Dependencies](https://david-dm.org/RocketChat/Rocket.Chat.ReactNative.svg)](https://david-dm.org/RocketChat/Rocket.Chat.ReactNative)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/bb15e2392a71473ea59d3f634f35c54e)](https://www.codacy.com/app/RocketChat/Rocket.Chat.ReactNative?utm_source=github.com&utm_medium=referral&utm_content=RocketChat/Rocket.Chat.ReactNative&utm_campaign=badger)
@ -10,74 +12,32 @@
**Supported Server Versions:** 0.58.0+ (We are working to support earlier versions)
# Installing Dependencies
# Installing dependencies
Follow the [React Native Getting Started Guide](https://facebook.github.io/react-native/docs/getting-started.html) for detailed instructions on setting up your local machine for development.
# Detailed configuration:
## Mac
- General requirements
- XCode 8.3
- Install required packages using homebrew:
```bash
$ brew install watchman
$ brew install yarn
```
- Clone repository and configure:
# How to run
- Clone repository and install dependencies:
```bash
$ git clone git@github.com:RocketChat/Rocket.Chat.ReactNative.git
$ cd Rocket.Chat.ReactNative
$ npm install
$ npm install -g react-native-cli
$ yarn
```
- Configuration
```bash
$ yarn fabric-ios --key="YOUR_API_KEY" --secret="YOUR_API_SECRET"
$ yarn fabric-android --key="YOUR_API_KEY" --secret="YOUR_API_SECRET"
```
- Run application
```bash
$ react-native run-ios
$ yarn ios
```
```bash
$ react-native run-android
$ yarn android
```
## Linux:
- General requiriments:
- JDK 7 or greater
- Android SDK
- Virtualbox
- An Android emulator: Genymotion or Android emulator. If using genymotion ensure that it uses existing adb tools (Settings: "Use custom Android SDK Tools")
- Install watchman (do this globally):
```bash
$ git clone https://github.com/facebook/watchman.git
$ cd watchman
$ git checkout master
$ ./autogen.sh
$ ./configure make
$ sudo make install
```
Configure your kernel to accept a lot of file watches, using a command like:
```bash
$ sudo sysctl -w fs.inotify.max_user_watches=1048576
```
- Clone repository and configure:
```bash
$ git clone git@github.com:RocketChat/Rocket.Chat.ReactNative.git
$ cd Rocket.Chat.ReactNative
$ npm install
$ npm install -g react-native-cli
```
- Run application
- Start emulator
- Start react packager: `$ react-native start`
- Run in emulator: `$ react-native run-android`
# Storybook
- General requirements
- Install storybook

View File

@ -1,32 +1,41 @@
import 'react-native';
import {View} from 'react-native';
import { Provider } from 'react-redux';
import { createStore, combineReducers } from 'redux';
const reducers = combineReducers({login:() => ({user: {}}), settings:() => ({})});
const store = createStore(reducers);
import React from 'react';
import RoomItem from '../app/components/RoomItem';
import RoomItem from '../app/presentation/RoomItem';
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
const date = new Date(2017, 10, 10, 10);
jest.mock('react-native-img-cache', () => { return { CachedImage: 'View' } });
it('renders correctly', () => {
expect(renderer.create(<RoomItem type="d" name="name" />).toJSON()).toMatchSnapshot();
expect(renderer.create(<Provider store={store}><View><RoomItem type="d" _updatedAt={date} name="name" /></View></Provider>).toJSON()).toMatchSnapshot();
});
it('render unread', () => {
expect(renderer.create(<RoomItem type="d" name="name" unread={1} />).toJSON()).toMatchSnapshot();
expect(renderer.create(<Provider store={store}><View><RoomItem type="d" _updatedAt={date} name="name" unread={1} /></View></Provider>).toJSON()).toMatchSnapshot();
});
it('render unread +999', () => {
expect(renderer.create(<RoomItem type="d" name="name" unread={1000} />).toJSON()).toMatchSnapshot();
expect(renderer.create(<Provider store={store}><View><RoomItem type="d" _updatedAt={date} name="name" unread={1000} /></View></Provider>).toJSON()).toMatchSnapshot();
});
it('render no icon', () => {
expect(renderer.create(<RoomItem type="X" name="name" />).toJSON()).toMatchSnapshot();
expect(renderer.create(<Provider store={store}><View><RoomItem type="X" _updatedAt={date} name="name" /></View></Provider>).toJSON()).toMatchSnapshot();
});
it('render private group', () => {
expect(renderer.create(<RoomItem type="g" name="private-group" /> ).toJSON()).toMatchSnapshot();
expect(renderer.create(<Provider store={store}><View><RoomItem type="g" _updatedAt={date} name="private-group" /> </View></Provider>).toJSON()).toMatchSnapshot();
});
it('render channel', () => {
expect(renderer.create(<RoomItem type="c" name="general" />).toJSON()).toMatchSnapshot();
expect(renderer.create(<Provider store={store}><View><RoomItem type="c" _updatedAt={date} name="general" /></View></Provider>).toJSON()).toMatchSnapshot();
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -45,12 +45,12 @@ android_library(
android_build_config(
name = "build_config",
package = "com.rocketchatrn",
package = "chat.rocket.reactnative",
)
android_resource(
name = "res",
package = "com.rocketchatrn",
package = "chat.rocket.reactnative",
res = "src/main/res",
)

View File

@ -94,22 +94,22 @@ android {
buildToolsVersion "25.0.1"
defaultConfig {
applicationId "com.rocketchatrn"
applicationId "chat.rocket.reactnative"
minSdkVersion 16
targetSdkVersion 22
versionCode 1
versionName "1.0"
versionCode VERSIONCODE as Integer
versionName "1.1"
ndk {
abiFilters "armeabi-v7a", "x86"
}
}
signingConfigs {
release {
if (project.hasProperty('ROCKETCHAT_RN_RELEASE_STORE_FILE')) {
storeFile file(ROCKETCHAT_RN_RELEASE_STORE_FILE)
storePassword ROCKETCHAT_RN_RELEASE_STORE_PASSWORD
keyAlias ROCKETCHAT_RN_RELEASE_KEY_ALIAS
keyPassword ROCKETCHAT_RN_RELEASE_KEY_PASSWORD
if (project.hasProperty('KEYSTORE')) {
storeFile file(KEYSTORE)
storePassword KEYSTORE_PASSWORD
keyAlias KEY_ALIAS
keyPassword KEY_PASSWORD
}
}
}
@ -143,17 +143,50 @@ android {
}
}
buildscript {
repositories {
maven { url 'https://maven.fabric.io/public' }
}
dependencies {
compile project(':react-native-navigation')
// These docs use an open ended version so that our plugin
// can be updated quickly in response to Android tooling updates
// We recommend changing it to the latest version from our changelog:
// https://docs.fabric.io/android/changelog.html#fabric-gradle-plugin
classpath 'io.fabric.tools:gradle:1.+'
}
}
apply plugin: 'io.fabric'
repositories {
maven { url 'https://maven.fabric.io/public' }
}
dependencies {
compile project(':react-native-fabric')
compile project(':react-native-audio')
compile project(":reactnativekeyboardinput")
compile project(':react-native-splash-screen')
compile project(':react-native-video')
compile project(':react-native-push-notification')
compile project(':react-native-svg')
compile project(':react-native-image-picker')
compile project(':react-native-vector-icons')
compile project(':react-native-fetch-blob')
compile project(':react-native-zeroconf')
compile project(':react-native-toast')
compile project(':realm')
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:23.0.1"
compile 'com.android.support:customtabs:23.0.1'
compile "com.facebook.react:react-native:+" // From node_modules
compile 'com.facebook.fresco:fresco:1.7.1'
compile 'com.facebook.fresco:animated-gif:1.7.1'
compile('com.crashlytics.sdk.android:crashlytics:2.9.1@aar') {
transitive = true;
}
}
// Run this once to be able to run the application with BUCK

View File

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.rocketchatrn"
package="chat.rocket.reactnative"
android:versionCode="1"
android:versionName="1.0">
@ -7,6 +7,16 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE"/>
<permission
android:name="${applicationId}.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="22" />
@ -28,6 +38,28 @@
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<receiver
android:name="com.google.android.gms.gcm.GcmReceiver"
android:exported="true"
android:permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="${applicationId}" />
</intent-filter>
</receiver>
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
<service android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationRegistrationService"/>
<service
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
android:exported="false" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</service>
</application>
</manifest>

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,56 @@
package chat.rocket.reactnative;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.support.customtabs.CustomTabsIntent;
import android.widget.Toast;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.List;
import chat.rocket.reactnative.R;
/**
* Launches custom tabs.
*/
public class CustomTabsAndroid extends ReactContextBaseJavaModule {
public CustomTabsAndroid(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "CustomTabsAndroid";
}
@ReactMethod
public void openURL(String url) throws NullPointerException {
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
CustomTabsIntent customTabsIntent = builder.build();
if (CustomTabsHelper.isChromeCustomTabsSupported(getReactApplicationContext())) {
customTabsIntent.launchUrl(getReactApplicationContext().getCurrentActivity(), Uri.parse(url));
} else {
//open in browser
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(url));
//ensure browser is present
final List<ResolveInfo> customTabsApps = getReactApplicationContext()
.getCurrentActivity().getPackageManager().queryIntentActivities(i, 0);
if (customTabsApps.size() > 0) {
getReactApplicationContext().startActivity(i);
} else {
// no browser
Toast.makeText(getReactApplicationContext(), R.string.no_browser_found, Toast.LENGTH_SHORT).show();
}
}
}
}

View File

@ -0,0 +1,24 @@
package chat.rocket.reactnative;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import java.util.List;
/**
* Contains helper methods for custom tabs.
*/
public class CustomTabsHelper {
private static final String SERVICE_ACTION = "android.support.customtabs.action.CustomTabsService";
private static final String CHROME_PACKAGE = "com.android.chrome";
public static boolean isChromeCustomTabsSupported(final Context context) {
Intent serviceIntent = new Intent(SERVICE_ACTION);
serviceIntent.setPackage(CHROME_PACKAGE);
List<ResolveInfo> resolveInfos = context.getPackageManager().queryIntentServices(serviceIntent, 0);
return !(resolveInfos == null || resolveInfos.isEmpty());
}
}

View File

@ -1,7 +1,26 @@
package com.rocketchatrn;
package chat.rocket.reactnative;
import com.reactnativenavigation.controllers.SplashActivity;
import android.os.Bundle;
import com.facebook.react.ReactActivity;
import org.devio.rn.splashscreen.SplashScreen;
import com.crashlytics.android.Crashlytics;
import io.fabric.sdk.android.Fabric;
public class MainActivity extends SplashActivity {
public class MainActivity extends ReactActivity {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "RocketChatRN";
}
@Override
protected void onCreate(Bundle savedInstanceState) {
SplashScreen.show(this);
super.onCreate(savedInstanceState);
Fabric.with(this, new Crashlytics());
}
}

View File

@ -1,9 +1,8 @@
package com.rocketchatrn;
package chat.rocket.reactnative;
import android.app.Application;
import com.facebook.react.ReactApplication;
// import com.reactnativenavigation.NavigationReactPackage;
import com.horcrux.svg.SvgPackage;
import com.imagepicker.ImagePickerPackage;
import com.oblador.vectoricons.VectorIconsPackage;
@ -14,31 +13,55 @@ import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
import com.reactnativenavigation.NavigationApplication;
import com.dieam.reactnativepushnotification.ReactNativePushNotificationPackage;
import com.brentvatne.react.ReactVideoPackage;
import com.remobile.toast.RCTToastPackage;
import com.wix.reactnativekeyboardinput.KeyboardInputPackage;
import com.rnim.rn.audio.ReactNativeAudioPackage;
import com.smixx.fabric.FabricPackage;
import java.util.Arrays;
import java.util.List;
import org.devio.rn.splashscreen.SplashScreenReactPackage;
public class MainApplication extends NavigationApplication {
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean isDebug() {
// Make sure you are using BuildConfig from your own application
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new SvgPackage(),
new ImagePickerPackage(),
new VectorIconsPackage(),
new RNFetchBlobPackage(),
new ZeroconfReactPackage(),
new RealmReactPackage()
new RealmReactPackage(),
new ReactNativePushNotificationPackage(),
new ReactVideoPackage(),
new SplashScreenReactPackage(),
new RCTToastPackage(),
new ReactNativeAudioPackage(),
new KeyboardInputPackage(MainApplication.this),
new RocketChatNativePackage(),
new FabricPackage()
);
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public List<ReactPackage> createAdditionalReactPackages() {
return getPackages();
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}

View File

@ -0,0 +1,33 @@
package chat.rocket.reactnative;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class RocketChatNativePackage implements ReactPackage {
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
List<ViewManager> managers = new ArrayList<>();
return managers;
}
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new CustomTabsAndroid(reactContext));
return modules;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 B

View File

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/launch_screen">
</LinearLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

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

View File

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

View File

@ -3,6 +3,7 @@
<!-- 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

@ -11,10 +11,10 @@
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.useDeprecatedNdk=true
VERSIONCODE=999999999

View File

@ -1,6 +1,14 @@
rootProject.name = 'RocketChatRN'
include ':react-native-navigation'
project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/android/app/')
include ':react-native-fabric'
project(':react-native-fabric').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fabric/android')
include ':react-native-audio'
project(':react-native-audio').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-audio/android')
include ':reactnativekeyboardinput'
project(':reactnativekeyboardinput').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keyboard-input/lib/android')
include ':react-native-splash-screen'
project(':react-native-splash-screen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-screen/android')
include ':react-native-video'
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':react-native-image-picker'
@ -13,5 +21,8 @@ include ':react-native-zeroconf'
project(':react-native-zeroconf').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-zeroconf/android')
include ':realm'
project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android')
include ':react-native-push-notification'
project(':react-native-push-notification').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-push-notification/android')
include ':react-native-toast'
project(':react-native-toast').projectDir = new File(settingsDir, '../node_modules/@remobile/react-native-toast/android')
include ':app'

13
app/ReactotronConfig.js Normal file
View File

@ -0,0 +1,13 @@
/* eslint-disable */
import Reactotron from 'reactotron-react-native';
import { reactotronRedux } from 'reactotron-redux';
import sagaPlugin from 'reactotron-redux-saga'
if (__DEV__) {
Reactotron
.configure()
.useReactNative()
.use(reactotronRedux())
.use(sagaPlugin())
.connect();
}

View File

@ -1,24 +1,107 @@
const REQUEST = 'REQUEST';
const SUCCESS = 'SUCCESS';
const FAILURE = 'FAILURE';
const defaultTypes = [REQUEST, SUCCESS, FAILURE];
function createRequestTypes(base, types = defaultTypes) {
const res = {};
types.forEach(type => res[type] = `${ base }_${ type }`);
types.forEach(type => (res[type] = `${ base }_${ type }`));
return res;
}
// Login events
export const LOGIN = createRequestTypes('LOGIN', [...defaultTypes, 'SET_TOKEN', 'SUBMIT']);
export const ROOMS = createRequestTypes('ROOMS');
export const LOGIN = createRequestTypes('LOGIN', [
...defaultTypes,
'SET_TOKEN',
'RESTORE_TOKEN',
'SUBMIT',
'REGISTER_SUBMIT',
'REGISTER_REQUEST',
'REGISTER_SUCCESS',
'REGISTER_INCOMPLETE',
'SET_USERNAME_SUBMIT',
'SET_USERNAME_REQUEST',
'SET_USERNAME_SUCCESS',
'OPEN',
'CLOSE',
'SET_SERVICES',
'REMOVE_SERVICES'
]);
export const FORGOT_PASSWORD = createRequestTypes('FORGOT_PASSWORD', [
...defaultTypes,
'INIT'
]);
export const USER = createRequestTypes('USER', ['SET']);
export const ROOMS = createRequestTypes('ROOMS', [...defaultTypes, 'SET_SEARCH']);
export const ROOM = createRequestTypes('ROOM', [
'ADD_USER_TYPING',
'REMOVE_USER_TYPING',
'SOMEONE_TYPING',
'OPEN',
'CLOSE',
'LEAVE',
'ERASE',
'USER_TYPING',
'MESSAGE_RECEIVED',
'SET_LAST_OPEN',
'LAYOUT_ANIMATION'
]);
export const APP = createRequestTypes('APP', ['READY', 'INIT']);
export const MESSAGES = createRequestTypes('MESSAGES');
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes, 'REQUEST_USERS', 'SUCCESS_USERS', 'FAILURE_USERS', 'SET_USERS']);
export const MESSAGES = createRequestTypes('MESSAGES', [
...defaultTypes,
'ACTIONS_SHOW',
'ACTIONS_HIDE',
'ERROR_ACTIONS_SHOW',
'ERROR_ACTIONS_HIDE',
'DELETE_REQUEST',
'DELETE_SUCCESS',
'DELETE_FAILURE',
'EDIT_INIT',
'EDIT_CANCEL',
'EDIT_REQUEST',
'EDIT_SUCCESS',
'EDIT_FAILURE',
'TOGGLE_STAR_REQUEST',
'TOGGLE_STAR_SUCCESS',
'TOGGLE_STAR_FAILURE',
'PERMALINK_REQUEST',
'PERMALINK_SUCCESS',
'PERMALINK_FAILURE',
'PERMALINK_CLEAR',
'TOGGLE_PIN_REQUEST',
'TOGGLE_PIN_SUCCESS',
'TOGGLE_PIN_FAILURE',
'SET_INPUT',
'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 NAVIGATION = createRequestTypes('NAVIGATION', ['SET']);
export const SERVER = createRequestTypes('SERVER', [...defaultTypes, 'SELECT', 'CHANGED', 'ADD']);
export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT']);
export const SERVER = createRequestTypes('SERVER', [
...defaultTypes,
'SELECT',
'CHANGED',
'ADD',
'GOTO_ADD'
]);
export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT', 'DISCONNECT_BY_USER']);
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 INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';

View File

@ -0,0 +1,8 @@
import * as types from './actionsTypes';
export function setActiveUser(data) {
return {
type: types.ACTIVE_USERS.SET,
data
};
}

View File

@ -25,3 +25,8 @@ export function disconnect(err) {
err
};
}
export function disconnect_by_user() {
return {
type: types.METEOR.DISCONNECT_BY_USER
};
}

View File

@ -21,7 +21,6 @@ export function createChannelFailure(err) {
};
}
export function createChannelRequestUsers(data) {
return {
type: types.CREATE_CHANNEL.REQUEST_USERS,
@ -49,3 +48,23 @@ export function createChannelFailureUsers(err) {
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

@ -19,12 +19,33 @@ export function setCurrentServer(server) {
};
}
export function addSettings(settings) {
return {
type: types.ADD_SETTINGS,
payload: settings
};
}
export function setAllSettings(settings) {
return {
type: types.SET_ALL_SETTINGS,
payload: settings
};
}
export function setAllPermissions(permissions) {
return {
type: types.SET_ALL_PERMISSIONS,
payload: permissions
};
}
export function setCustomEmojis(emojis) {
return {
type: types.SET_CUSTOM_EMOJIS,
payload: emojis
};
}
export function login() {
return {
type: 'LOGIN'

View File

@ -13,6 +13,50 @@ export function loginRequest(credentials) {
};
}
export function registerSubmit(credentials) {
return {
type: types.LOGIN.REGISTER_SUBMIT,
credentials
};
}
export function registerRequest(credentials) {
return {
type: types.LOGIN.REGISTER_REQUEST,
credentials
};
}
export function registerSuccess(credentials) {
return {
type: types.LOGIN.REGISTER_SUCCESS,
credentials
};
}
export function registerIncomplete() {
return {
type: types.LOGIN.REGISTER_INCOMPLETE
};
}
export function setUsernameSubmit(credentials) {
return {
type: types.LOGIN.SET_USERNAME_SUBMIT,
credentials
};
}
export function setUsernameRequest(credentials) {
return {
type: types.LOGIN.SET_USERNAME_REQUEST,
credentials
};
}
export function setUsernameSuccess() {
return {
type: types.LOGIN.SET_USERNAME_SUCCESS
};
}
export function loginSuccess(user) {
return {
type: types.LOGIN.SUCCESS,
@ -31,8 +75,14 @@ export function loginFailure(err) {
export function setToken(user = {}) {
return {
type: types.LOGIN.SET_TOKEN,
token: user.token,
user
...user
};
}
export function restoreToken(token) {
return {
type: types.LOGIN.RESTORE_TOKEN,
token
};
}
@ -41,3 +91,61 @@ export function logout() {
type: types.LOGOUT
};
}
export function forgotPasswordInit() {
return {
type: types.FORGOT_PASSWORD.INIT
};
}
export function forgotPasswordRequest(email) {
return {
type: types.FORGOT_PASSWORD.REQUEST,
email
};
}
export function forgotPasswordSuccess() {
return {
type: types.FORGOT_PASSWORD.SUCCESS
};
}
export function forgotPasswordFailure(err) {
return {
type: types.FORGOT_PASSWORD.FAILURE,
err
};
}
export function setUser(action) {
return {
type: types.USER.SET,
...action
};
}
export function open() {
return {
type: types.LOGIN.OPEN
};
}
export function close() {
return {
type: types.LOGIN.CLOSE
};
}
export function setLoginServices(data) {
return {
type: types.LOGIN.SET_SERVICES,
data
};
}
export function removeLoginServices() {
return {
type: types.LOGIN.REMOVE_SERVICES
};
}

View File

@ -0,0 +1,21 @@
import * as types from './actionsTypes';
export function openMentionedMessages(rid) {
return {
type: types.MENTIONED_MESSAGES.OPEN,
rid
};
}
export function closeMentionedMessages() {
return {
type: types.MENTIONED_MESSAGES.CLOSE
};
}
export function mentionedMessagesReceived(messages) {
return {
type: types.MENTIONED_MESSAGES.MESSAGES_RECEIVED,
messages
};
}

View File

@ -19,3 +19,167 @@ export function messagesFailure(err) {
err
};
}
export function actionsShow(actionMessage) {
return {
type: types.MESSAGES.ACTIONS_SHOW,
actionMessage
};
}
export function actionsHide() {
return {
type: types.MESSAGES.ACTIONS_HIDE
};
}
export function errorActionsShow(actionMessage) {
return {
type: types.MESSAGES.ERROR_ACTIONS_SHOW,
actionMessage
};
}
export function errorActionsHide() {
return {
type: types.MESSAGES.ERROR_ACTIONS_HIDE
};
}
export function deleteRequest(message) {
return {
type: types.MESSAGES.DELETE_REQUEST,
message
};
}
export function deleteSuccess() {
return {
type: types.MESSAGES.DELETE_SUCCESS
};
}
export function deleteFailure() {
return {
type: types.MESSAGES.DELETE_FAILURE
};
}
export function editInit(message) {
return {
type: types.MESSAGES.EDIT_INIT,
message
};
}
export function editCancel() {
return {
type: types.MESSAGES.EDIT_CANCEL
};
}
export function editRequest(message) {
return {
type: types.MESSAGES.EDIT_REQUEST,
message
};
}
export function editSuccess() {
return {
type: types.MESSAGES.EDIT_SUCCESS
};
}
export function editFailure() {
return {
type: types.MESSAGES.EDIT_FAILURE
};
}
export function toggleStarRequest(message) {
return {
type: types.MESSAGES.TOGGLE_STAR_REQUEST,
message
};
}
export function toggleStarSuccess() {
return {
type: types.MESSAGES.TOGGLE_STAR_SUCCESS
};
}
export function toggleStarFailure() {
return {
type: types.MESSAGES.TOGGLE_STAR_FAILURE
};
}
export function permalinkRequest(message) {
return {
type: types.MESSAGES.PERMALINK_REQUEST,
message
};
}
export function permalinkSuccess(permalink) {
return {
type: types.MESSAGES.PERMALINK_SUCCESS,
permalink
};
}
export function permalinkFailure(err) {
return {
type: types.MESSAGES.PERMALINK_FAILURE,
err
};
}
export function permalinkClear() {
return {
type: types.MESSAGES.PERMALINK_CLEAR
};
}
export function togglePinRequest(message) {
return {
type: types.MESSAGES.TOGGLE_PIN_REQUEST,
message
};
}
export function togglePinSuccess() {
return {
type: types.MESSAGES.TOGGLE_PIN_SUCCESS
};
}
export function togglePinFailure(err) {
return {
type: types.MESSAGES.TOGGLE_PIN_FAILURE,
err
};
}
export function setInput(message) {
return {
type: types.MESSAGES.SET_INPUT,
message
};
}
export function clearInput() {
return {
type: types.MESSAGES.CLEAR_INPUT
};
}
export function toggleReactionPicker(message) {
return {
type: types.MESSAGES.TOGGLE_REACTION_PICKER,
message
};
}

View File

@ -0,0 +1,28 @@
import * as types from './actionsTypes';
export function openPinnedMessages(rid) {
return {
type: types.PINNED_MESSAGES.OPEN,
rid
};
}
export function closePinnedMessages() {
return {
type: types.PINNED_MESSAGES.CLOSE
};
}
export function pinnedMessagesReceived(messages) {
return {
type: types.PINNED_MESSAGES.MESSAGES_RECEIVED,
messages
};
}
export function pinnedMessageUnpinned(messageId) {
return {
type: types.PINNED_MESSAGES.MESSAGE_UNPINNED,
messageId
};
}

8
app/actions/roles.js Normal file
View File

@ -0,0 +1,8 @@
import * as types from './actionsTypes';
export function setRoles(data) {
return {
type: types.ROLES.SET,
data
};
}

77
app/actions/room.js Normal file
View File

@ -0,0 +1,77 @@
import * as types from './actionsTypes';
export function removeUserTyping(username) {
return {
type: types.ROOM.REMOVE_USER_TYPING,
username
};
}
export function someoneTyping(data) {
return {
type: types.ROOM.SOMEONE_TYPING,
...data
};
}
export function addUserTyping(username) {
return {
type: types.ROOM.ADD_USER_TYPING,
username
};
}
export function openRoom(room) {
return {
type: types.ROOM.OPEN,
room
};
}
export function closeRoom() {
return {
type: types.ROOM.CLOSE
};
}
export function leaveRoom(rid) {
return {
type: types.ROOM.LEAVE,
rid
};
}
export function eraseRoom(rid) {
return {
type: types.ROOM.ERASE,
rid
};
}
export function userTyping(status = true) {
return {
type: types.ROOM.USER_TYPING,
status
};
}
export function roomMessageReceived(message) {
return {
type: types.ROOM.MESSAGE_RECEIVED,
message
};
}
export function setLastOpen(date = new Date()) {
return {
type: types.ROOM.SET_LAST_OPEN,
date
};
}
export function layoutAnimation() {
return {
type: types.ROOM.LAYOUT_ANIMATION
};
}

21
app/actions/roomFiles.js Normal file
View File

@ -0,0 +1,21 @@
import * as types from './actionsTypes';
export function openRoomFiles(rid) {
return {
type: types.ROOM_FILES.OPEN,
rid
};
}
export function closeRoomFiles() {
return {
type: types.ROOM_FILES.CLOSE
};
}
export function roomFilesReceived(messages) {
return {
type: types.ROOM_FILES.MESSAGES_RECEIVED,
messages
};
}

View File

@ -1,5 +1,6 @@
import * as types from './actionsTypes';
export function roomsRequest() {
return {
type: types.ROOMS.REQUEST
@ -18,3 +19,10 @@ export function roomsFailure(err) {
err
};
}
export function setSearch(searchText) {
return {
type: types.ROOMS.SET_SEARCH,
searchText
};
}

View File

@ -41,3 +41,9 @@ export function changedServer(server) {
server
};
}
export function gotoAddServer() {
return {
type: SERVER.GOTO_ADD
};
}

View File

@ -0,0 +1,21 @@
import * as types from './actionsTypes';
export function openSnippetedMessages(rid) {
return {
type: types.SNIPPETED_MESSAGES.OPEN,
rid
};
}
export function closeSnippetedMessages() {
return {
type: types.SNIPPETED_MESSAGES.CLOSE
};
}
export function snippetedMessagesReceived(messages) {
return {
type: types.SNIPPETED_MESSAGES.MESSAGES_RECEIVED,
messages
};
}

View File

@ -0,0 +1,28 @@
import * as types from './actionsTypes';
export function openStarredMessages(rid) {
return {
type: types.STARRED_MESSAGES.OPEN,
rid
};
}
export function closeStarredMessages() {
return {
type: types.STARRED_MESSAGES.CLOSE
};
}
export function starredMessagesReceived(messages) {
return {
type: types.STARRED_MESSAGES.MESSAGES_RECEIVED,
messages
};
}
export function starredMessageUnstarred(messageId) {
return {
type: types.STARRED_MESSAGES.MESSAGE_UNSTARRED,
messageId
};
}

View File

@ -0,0 +1,64 @@
import { View, Animated } from 'react-native';
import PropTypes from 'prop-types';
import React from 'react';
export default class Panel extends React.Component {
static propTypes = {
open: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
style: PropTypes.object
}
constructor(props) {
super(props);
this.state = {
animation: new Animated.Value()
};
this.first = true;
this.open = false;
this.opacity = 0;
}
componentDidMount() {
const initialValue = !this.props.open ? this.height : 0;
this.state.animation.setValue(initialValue);
}
componentWillReceiveProps(nextProps) {
if (this.first) {
this.first = false;
if (!this.props.open) {
this.state.animation.setValue(0);
return;
}
}
if (this.open === nextProps.open) {
return;
}
this.open = nextProps.open;
const initialValue = !nextProps.open ? this.height : 0;
const finalValue = !nextProps.open ? 0 : this.height;
this.state.animation.setValue(initialValue);
Animated.timing(
this.state.animation,
{
toValue: finalValue,
duration: 150,
useNativeDriver: true
}
).start();
}
set _height(h) {
this.height = h || this.height;
}
render() {
return (
<Animated.View
style={[{ height: this.state.animation }, this.props.style]}
>
<View onLayout={({ nativeEvent }) => this._height = nativeEvent.layout.height} style={{ position: !this.first ? 'relative' : 'absolute' }}>
{this.props.children}
</View>
</Animated.View>
);
}
}

View File

@ -17,9 +17,6 @@ export default class Fade extends React.Component {
this.state = {
visible: props.visible
};
}
componentWillMount() {
this._visibility = new Animated.Value(this.props.visible ? 1 : 0);
}
@ -29,7 +26,8 @@ export default class Fade extends React.Component {
}
Animated.timing(this._visibility, {
toValue: nextProps.visible ? 1 : 0,
duration: 300
duration: 300,
useNativeDriver: true
}).start(() => {
this.setState({ visible: nextProps.visible });
});

View File

@ -1,22 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { KeyboardAvoidingView, Platform } from 'react-native';
export default class KeyboardView extends React.PureComponent {
static propTypes = {
style: KeyboardAvoidingView.propTypes.style,
keyboardVerticalOffset: PropTypes.number,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
])
}
render() {
return (
<KeyboardAvoidingView style={this.props.style} behavior={Platform.OS === 'ios' ? 'padding' : null} keyboardVerticalOffset={this.props.keyboardVerticalOffset}>
{this.props.children}
</KeyboardAvoidingView>
);
}
}

View File

@ -1,88 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, Text, StyleSheet } from 'react-native';
import { emojify } from 'react-emojione';
import Markdown from 'react-native-easy-markdown';
import moment from 'moment';
import Avatar from './avatar';
import Card from './message/card';
const styles = StyleSheet.create({
content: {
flexGrow: 1
},
message: {
padding: 12,
paddingTop: 6,
paddingBottom: 6,
flexDirection: 'row',
transform: [{ scaleY: -1 }]
},
texts: {
flex: 1
},
msg: {
flex: 1
},
username: {
fontWeight: 'bold'
},
usernameView: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 2
},
alias: {
fontSize: 10,
color: '#888',
paddingLeft: 5
},
time: {
fontSize: 10,
color: '#888',
paddingLeft: 5
}
});
export default class Message extends React.PureComponent {
static propTypes = {
item: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
Message_TimeFormat: PropTypes.string.isRequired
}
attachments() {
return this.props.item.attachments.length ? <Card data={this.props.item.attachments[0]} /> : null;
}
render() {
const { item } = this.props;
const extraStyle = {};
if (item.temp) {
extraStyle.opacity = 0.3;
}
const msg = emojify(item.msg, { output: 'unicode' });
const username = item.alias || item.u.username;
const time = moment(item.ts).format(this.props.Message_TimeFormat);
return (
<View style={[styles.message, extraStyle]}>
<Avatar style={{ marginRight: 10 }} text={item.avatar ? '' : username} size={40} baseUrl={this.props.baseUrl} avatar={item.avatar} />
<View style={[styles.content]}>
<View style={styles.usernameView}>
<Text onPress={this._onPress} style={styles.username}>
{username}
</Text>
{item.alias && <Text style={styles.alias}>@{item.u.username}</Text>}<Text style={styles.time}>{time}</Text>
</View>
{this.attachments()}
<Markdown>
{msg}
</Markdown>
</View>
</View>
);
}
}

View File

@ -1,94 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, TextInput, StyleSheet } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import ImagePicker from 'react-native-image-picker';
import RocketChat from '../lib/rocketchat';
const styles = StyleSheet.create({
textBox: {
paddingTop: 1,
borderTopWidth: 1,
borderTopColor: '#ccc',
backgroundColor: '#fff',
flexDirection: 'row',
alignItems: 'center'
},
textBoxInput: {
height: 40,
alignSelf: 'stretch',
backgroundColor: '#fff',
flexGrow: 1
},
fileButton: {
color: '#aaa',
paddingLeft: 23,
paddingRight: 20,
paddingTop: 10,
paddingBottom: 10,
fontSize: 20
}
});
export default class MessageBox extends React.PureComponent {
static propTypes = {
onSubmit: PropTypes.func.isRequired,
rid: PropTypes.string.isRequired
}
submit(message) {
const text = message;
if (text.trim() === '') {
return;
}
if (this.component) {
this.component.setNativeProps({ text: '' });
}
this.props.onSubmit(text);
}
addFile = () => {
const options = {
customButtons: [{
name: 'import', title: 'Import File From'
}]
};
ImagePicker.showImagePicker(options, (response) => {
if (response.didCancel) {
console.log('User cancelled image picker');
} else if (response.error) {
console.log('ImagePicker Error: ', response.error);
} else if (response.customButton) {
console.log('User tapped custom button: ', response.customButton);
} else {
const fileInfo = {
name: response.fileName,
size: response.fileSize,
type: response.type || 'image/jpeg',
// description: '',
store: 'Uploads'
};
RocketChat.sendFileMessage(this.props.rid, fileInfo, response.data);
}
});
}
render() {
return (
<View style={styles.textBox}>
<Icon style={styles.fileButton} name='add-circle-outline' onPress={this.addFile} />
<TextInput
ref={component => this.component = component}
style={styles.textBoxInput}
returnKeyType='send'
onSubmitEditing={event => this.submit(event.nativeEvent.text)}
blurOnSubmit={false}
placeholder='New message'
underlineColorAndroid='transparent'
defaultValue={''}
/>
</View>
);
}
}

View File

@ -1,123 +0,0 @@
import React from 'react';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import PropTypes from 'prop-types';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import Avatar from './avatar';
import avatarInitialsAndColor from '../utils/avatarInitialsAndColor';
const styles = StyleSheet.create({
container: {
// flex: 1,
flexDirection: 'row',
paddingLeft: 16,
paddingRight: 16,
height: 56,
alignItems: 'center'
},
number: {
minWidth: 20,
borderRadius: 5,
backgroundColor: '#1d74f5',
color: '#fff',
textAlign: 'center',
overflow: 'hidden',
fontSize: 14,
paddingLeft: 5,
paddingRight: 5
},
roomName: {
flex: 1,
fontSize: 16,
color: '#444',
marginLeft: 16,
marginRight: 4
},
iconContainer: {
height: 40,
width: 40,
borderRadius: 20,
overflow: 'hidden',
justifyContent: 'center',
alignItems: 'center'
},
icon: {
fontSize: 20,
color: '#fff'
},
avatar: {
width: 40,
height: 40,
position: 'absolute',
borderRadius: 20
},
avatarInitials: {
fontSize: 20,
color: '#ffffff'
}
});
export default class RoomItem extends React.PureComponent {
static propTypes = {
type: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
unread: PropTypes.number,
baseUrl: PropTypes.string,
onPress: PropTypes.func
}
get icon() {
const { type, name, baseUrl } = this.props;
const icon = {
d: 'at',
c: 'pound',
p: 'lock',
l: 'account'
}[type];
if (!icon) {
return null;
}
if (type === 'd') {
return (
<Avatar text={name} baseUrl={baseUrl} size={40} borderRadius={20} />
);
}
const { color } = avatarInitialsAndColor(name);
return (
<View style={[styles.iconContainer, { backgroundColor: color }]}>
<MaterialCommunityIcons name={icon} style={styles.icon} />
</View>
);
}
renderNumber = (unread) => {
if (!unread || unread <= 0) {
return;
}
if (unread >= 1000) {
unread = '999+';
}
return (
<Text style={styles.number}>
{ unread }
</Text>
);
}
render() {
const { unread, name } = this.props;
return (
<TouchableOpacity onPress={this.props.onPress} style={styles.container}>
{this.icon}
<Text style={styles.roomName} ellipsizeMode='tail' numberOfLines={1}>{ name }</Text>
{this.renderNumber(unread)}
</TouchableOpacity>
);
}
}

View File

@ -1,51 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StyleSheet, Text, View } from 'react-native';
import { CachedImage } from 'react-native-img-cache';
import avatarInitialsAndColor from '../utils/avatarInitialsAndColor';
const styles = StyleSheet.create({
iconContainer: {
overflow: 'hidden',
justifyContent: 'center',
alignItems: 'center'
},
avatar: {
position: 'absolute'
},
avatarInitials: {
color: '#ffffff'
} });
class Avatar extends React.PureComponent {
render() {
const { text = '', size = 25, baseUrl = this.props.baseUrl,
borderRadius = 5, style, avatar } = this.props;
const { initials, color } = avatarInitialsAndColor(`${ text }`);
return (
<View style={[styles.iconContainer, {
backgroundColor: color,
width: size,
height: size,
borderRadius
}, style]}
>
<Text style={[styles.avatarInitials, { fontSize: size / 2 }]}>{initials}</Text>
{ (avatar || baseUrl) && <CachedImage
style={[styles.avatar, { width: size,
height: size }]}
source={{ uri: avatar || `${ baseUrl }/avatar/${ text }` }}
/>}
</View>);
}
}
Avatar.propTypes = {
style: PropTypes.object,
baseUrl: PropTypes.string,
text: PropTypes.string.isRequired,
avatar: PropTypes.string,
size: PropTypes.number,
borderRadius: PropTypes.number
};
export default Avatar;

View File

@ -1,88 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Meteor from 'react-native-meteor';
import { connect } from 'react-redux';
import { CachedImage } from 'react-native-img-cache';
import { Text, TouchableOpacity } from 'react-native';
import { Navigation } from 'react-native-navigation';
import {
Card,
CardImage,
// CardTitle,
CardContent
// CardAction
} from 'react-native-card-view';
import RocketChat from '../../lib/rocketchat';
const close = () => Navigation.dismissModal({
animationType: 'slide-down'
});
const CustomButton = ({ text }) => (
<TouchableOpacity onPress={close}>
<Text style={{ color: 'blue' }}>{text}</Text>
</TouchableOpacity>
);
CustomButton.propTypes = {
text: PropTypes.string
};
Navigation.registerComponent('CustomButton', () => CustomButton);
@connect(state => ({
base: state.settings.Site_Url,
canShowList: state.login.token.length || state.login.user.token
}))
export default class Cards extends React.PureComponent {
static propTypes = {
data: PropTypes.object.isRequired,
base: PropTypes.string
}
constructor() {
super();
const user = Meteor.user();
this.state = {};
RocketChat.getUserToken().then((token) => {
this.setState({ img: `${ this.props.base }${ this.props.data.image_url }?rc_uid=${ user._id }&rc_token=${ token }` });
});
}
_onPressButton() {
Navigation.showModal({
screen: 'Photo',
title: this.props.data.title, // title of the screen as appears in the nav bar (optional)
passProps: { image: this.state.img },
// navigatorStyle: {}, // override the navigator style for the screen, see "Styling the navigator" below (optional)
navigatorButtons: {
leftButtons: [{
id: 'custom-button',
component: 'CustomButton',
passProps: {
text: 'close'
}
}]
}, // override the nav buttons for the screen, see "Adding buttons to the navigator" below (optional)
animationType: 'slide-up' // 'none' / 'slide-up' , appear animation for the modal (optional, default 'slide-up')
});
}
render() {
return this.state.img ? (
<TouchableOpacity onPress={() => this._onPressButton()}>
<Card>
<CardImage style={{ width: 256, height: 256 }}>
<CachedImage
style={{ width: 256, height: 256 }}
source={{ uri: encodeURI(this.state.img) }}
/>
</CardImage>
<CardContent>
<Text style={[{ fontSize: 12, alignSelf: 'center', fontStyle: 'italic' }]}>{this.props.data.title}</Text>
<Text style={{ alignSelf: 'center', fontWeight: 'bold' }}>{this.props.data.description}</Text>
</CardContent>
</Card>
</TouchableOpacity>
) :
<Text style={[{ fontSize: 12, alignSelf: 'center', fontStyle: 'italic' }]}>{this.props.data.title}</Text>;
}
}

View File

@ -1,2 +1,9 @@
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 STATUS_COLORS = {
online: '#2de0a5',
busy: COLOR_DANGER,
away: '#ffd21f',
offline: '#cbced1'
};

View File

@ -0,0 +1,5 @@
export default {
SENT: 0,
TEMP: 1,
ERROR: 2
};

View File

@ -1,2 +1,5 @@
export const SET_CURRENT_SERVER = 'SET_CURRENT_SERVER';
export const SET_ALL_SETTINGS = 'SET_ALL_SETTINGS';
export const SET_ALL_PERMISSIONS = 'SET_ALL_PERMISSIONS';
export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
export const ADD_SETTINGS = 'ADD_SETTINGS';

88
app/containers/Avatar.js Normal file
View File

@ -0,0 +1,88 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { StyleSheet, Text, View, ViewPropTypes } from 'react-native';
import { CachedImage } from 'react-native-img-cache';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import avatarInitialsAndColor from '../utils/avatarInitialsAndColor';
const styles = StyleSheet.create({
iconContainer: {
// overflow: 'hidden',
justifyContent: 'center',
alignItems: 'center'
},
avatar: {
position: 'absolute'
},
avatarInitials: {
color: '#ffffff'
}
});
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}))
export default class Avatar extends React.PureComponent {
static propTypes = {
style: ViewPropTypes.style,
baseUrl: PropTypes.string,
text: PropTypes.string.isRequired,
avatar: PropTypes.string,
size: PropTypes.number,
borderRadius: PropTypes.number,
type: PropTypes.string,
children: PropTypes.object
};
render() {
const {
text = '', size = 25, baseUrl, borderRadius = 4, style, avatar, type = 'd'
} = this.props;
const { initials, color } = avatarInitialsAndColor(`${ text }`);
const iconContainerStyle = {
backgroundColor: color,
width: size,
height: size,
borderRadius
};
const avatarInitialsStyle = {
fontSize: size / 2
};
const avatarStyle = {
width: size,
height: size,
borderRadius
};
if (type === 'd') {
const uri = avatar || `${ baseUrl }/avatar/${ text }`;
const image = (avatar || baseUrl) && (
<CachedImage
style={[styles.avatar, avatarStyle]}
source={{ uri }}
/>
);
return (
<View style={[styles.iconContainer, iconContainerStyle, style]}>
<Text style={[styles.avatarInitials, avatarInitialsStyle]} allowFontScaling={false}>{initials}</Text>
{image}
{this.props.children}
</View>);
}
const icon = {
c: 'pound',
p: 'lock',
l: 'account'
}[type];
return (
<View style={[styles.iconContainer, iconContainerStyle, style]}>
<MaterialCommunityIcons name={icon} style={[styles.avatarInitials, avatarInitialsStyle]} />
</View>
);
}
}

View File

@ -30,9 +30,16 @@ export default class Banner extends React.PureComponent {
authenticating: PropTypes.bool,
offline: PropTypes.bool
}
render() {
const { connecting, authenticating, offline } = this.props;
if (offline) {
return (
<View style={[styles.bannerContainer, { backgroundColor: 'red' }]}>
<Text style={[styles.bannerText, { color: '#a00' }]}>offline...</Text>
</View>
);
}
if (connecting) {
return (
<View style={[styles.bannerContainer, { backgroundColor: '#0d0' }]}>
@ -48,13 +55,7 @@ export default class Banner extends React.PureComponent {
</View>
);
}
if (offline) {
return (
<View style={[styles.bannerContainer, { backgroundColor: 'red' }]}>
<Text style={[styles.bannerText, { color: '#a00' }]}>offline...</Text>
</View>
);
}
return null;
}
}

View File

@ -0,0 +1,28 @@
import React from 'react';
import { ViewPropTypes } from 'react-native';
import PropTypes from 'prop-types';
import { CachedImage } from 'react-native-img-cache';
import { connect } from 'react-redux';
@connect(state => ({
baseUrl: state.settings.Site_Url
}))
export default class CustomEmoji extends React.Component {
static propTypes = {
baseUrl: PropTypes.string.isRequired,
emoji: PropTypes.object.isRequired,
style: ViewPropTypes.style
}
shouldComponentUpdate() {
return false;
}
render() {
const { baseUrl, emoji, style } = this.props;
return (
<CachedImage
style={style}
source={{ uri: `${ baseUrl }/emoji-custom/${ encodeURIComponent(emoji.content || emoji.name) }.${ emoji.extension }` }}
/>
);
}
}

View File

@ -0,0 +1,71 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text, TouchableOpacity, Platform } from 'react-native';
import { emojify } from 'react-emojione';
import { responsive } from 'react-native-responsive-ui';
import { OptimizedFlatList } from 'react-native-optimized-flatlist';
import styles from './styles';
import CustomEmoji from './CustomEmoji';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
const emojisPerRow = Platform.OS === 'ios' ? 8 : 9;
const renderEmoji = (emoji, size) => {
if (emoji.isCustom) {
return <CustomEmoji style={[styles.customCategoryEmoji, { height: size - 8, width: size - 8 }]} emoji={emoji} />;
}
return (
<Text style={[styles.categoryEmoji, { height: size, width: size, fontSize: size - 14 }]}>
{emojify(`:${ emoji }:`, { output: 'unicode' })}
</Text>
);
};
@responsive
export default class EmojiCategory extends React.Component {
static propTypes = {
emojis: PropTypes.any,
window: PropTypes.any,
onEmojiSelected: PropTypes.func,
emojisPerRow: PropTypes.number,
width: PropTypes.number
};
constructor(props) {
super(props);
const { width, height } = this.props.window;
this.size = Math.min(this.props.width || width, height) / (this.props.emojisPerRow || emojisPerRow);
this.emojis = props.emojis;
}
shouldComponentUpdate() {
return false;
}
renderItem(emoji, size) {
return (
<TouchableOpacity
activeOpacity={0.7}
key={emoji.isCustom ? emoji.content : emoji}
onPress={() => this.props.onEmojiSelected(emoji)}
>
{renderEmoji(emoji, size)}
</TouchableOpacity>);
}
render() {
return (
<OptimizedFlatList
keyExtractor={item => (item.isCustom && item.content) || item}
data={this.props.emojis}
renderItem={({ item }) => this.renderItem(item, this.size)}
numColumns={emojisPerRow}
initialNumToRender={45}
getItemLayout={(data, index) => ({ length: this.size, offset: this.size * index, index })}
removeClippedSubviews
{...scrollPersistTaps}
/>
);
}
}

View File

@ -0,0 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, TouchableOpacity, Text } from 'react-native';
import styles from './styles';
export default class TabBar extends React.PureComponent {
static propTypes = {
goToPage: PropTypes.func,
activeTab: PropTypes.number,
tabs: PropTypes.array,
tabEmojiStyle: PropTypes.object
}
render() {
return (
<View style={styles.tabsContainer}>
{this.props.tabs.map((tab, i) => (
<TouchableOpacity
activeOpacity={0.7}
key={tab}
onPress={() => this.props.goToPage(i)}
style={styles.tab}
>
<Text style={[styles.tabEmoji, this.props.tabEmojiStyle]}>{tab}</Text>
{this.props.activeTab === i ? <View style={styles.activeTabLine} /> : <View style={styles.tabLine} />}
</TouchableOpacity>
))}
</View>
);
}
}

View File

@ -0,0 +1,44 @@
const list = ['frequentlyUsed', 'custom', 'people', 'nature', 'food', 'activity', 'travel', 'objects', 'symbols', 'flags'];
const tabs = [
{
tabLabel: '🕒',
category: list[0]
},
{
tabLabel: '🚀',
category: list[1]
},
{
tabLabel: '😃',
category: list[2]
},
{
tabLabel: '🐶',
category: list[3]
},
{
tabLabel: '🍔',
category: list[4]
},
{
tabLabel: '⚽',
category: list[5]
},
{
tabLabel: '🚌',
category: list[6]
},
{
tabLabel: '💡',
category: list[7]
},
{
tabLabel: '💛',
category: list[8]
},
{
tabLabel: '🏁',
category: list[9]
}
];
export default { list, tabs };

View File

@ -0,0 +1,141 @@
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 { emojify } from 'react-emojione';
import TabBar from './TabBar';
import EmojiCategory from './EmojiCategory';
import styles from './styles';
import categories from './categories';
import database from '../../lib/realm';
import { emojisByCategory } from '../../emojis';
const scrollProps = {
keyboardShouldPersistTaps: 'always',
keyboardDismissMode: 'none'
};
export default class EmojiPicker extends Component {
static propTypes = {
onEmojiSelected: PropTypes.func,
tabEmojiStyle: PropTypes.object,
emojisPerRow: PropTypes.number,
width: PropTypes.number
};
constructor(props) {
super(props);
this.state = {
frequentlyUsed: [],
customEmojis: []
};
this.frequentlyUsed = database.objects('frequentlyUsedEmoji').sorted('count', true);
this.customEmojis = database.objects('customEmojis');
this.updateFrequentlyUsed = this.updateFrequentlyUsed.bind(this);
this.updateCustomEmojis = this.updateCustomEmojis.bind(this);
}
//
// shouldComponentUpdate(nextProps) {
// return false;
// }
componentDidMount() {
requestAnimationFrame(() => this.setState({ show: true }));
this.frequentlyUsed.addListener(this.updateFrequentlyUsed);
this.customEmojis.addListener(this.updateCustomEmojis);
this.updateFrequentlyUsed();
this.updateCustomEmojis();
}
componentWillUnmount() {
this.frequentlyUsed.removeAllListeners();
this.customEmojis.removeAllListeners();
}
onEmojiSelected(emoji) {
if (emoji.isCustom) {
const count = this._getFrequentlyUsedCount(emoji.content);
this._addFrequentlyUsed({
content: emoji.content, extension: emoji.extension, count, isCustom: true
});
this.props.onEmojiSelected(`:${ emoji.content }:`);
} else {
const content = emoji;
const count = this._getFrequentlyUsedCount(content);
this._addFrequentlyUsed({ content, count, isCustom: false });
const shortname = `:${ emoji }:`;
this.props.onEmojiSelected(emojify(shortname, { output: 'unicode' }), shortname);
}
}
_addFrequentlyUsed = (emoji) => {
database.write(() => {
database.create('frequentlyUsedEmoji', emoji, true);
});
}
_getFrequentlyUsedCount = (content) => {
const emojiRow = this.frequentlyUsed.filtered('content == $0', content);
return emojiRow.length ? emojiRow[0].count + 1 : 1;
}
updateFrequentlyUsed() {
const frequentlyUsed = _.map(this.frequentlyUsed.slice(), (item) => {
if (item.isCustom) {
return item;
}
return emojify(`${ item.content }`, { output: 'unicode' });
});
this.setState({ frequentlyUsed });
}
updateCustomEmojis() {
const customEmojis = _.map(this.customEmojis.slice(), item =>
({ content: item.name, extension: item.extension, isCustom: true }));
this.setState({ customEmojis });
}
renderCategory(category, i) {
let emojis = [];
if (i === 0) {
emojis = this.state.frequentlyUsed;
} else if (i === 1) {
emojis = this.state.customEmojis;
} else {
emojis = emojisByCategory[category];
}
return (
<EmojiCategory
emojis={emojis}
onEmojiSelected={emoji => this.onEmojiSelected(emoji)}
style={styles.categoryContainer}
size={this.props.emojisPerRow}
width={this.props.width}
/>
);
}
render() {
if (!this.state.show) {
return null;
}
return (
// <View style={styles.container}>
<ScrollableTabView
renderTabBar={() => <TabBar tabEmojiStyle={this.props.tabEmojiStyle} />}
contentProps={scrollProps}
>
{
categories.tabs.map((tab, i) => (
<ScrollView
key={tab.category}
tabLabel={tab.tabLabel}
{...scrollProps}
>
{this.renderCategory(tab.category, i)}
</ScrollView>
))
}
</ScrollableTabView>
// </View>
);
}
}

View File

@ -0,0 +1,57 @@
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
container: {
flex: 1
},
tabsContainer: {
height: 45,
flexDirection: 'row',
paddingTop: 5
},
tab: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingBottom: 10
},
tabEmoji: {
fontSize: 20,
color: 'black'
},
activeTabLine: {
position: 'absolute',
left: 0,
right: 0,
height: 2,
backgroundColor: '#007aff',
bottom: 0
},
tabLine: {
position: 'absolute',
left: 0,
right: 0,
height: 2,
backgroundColor: 'rgba(0,0,0,0.05)',
bottom: 0
},
categoryContainer: {
flex: 1,
alignItems: 'flex-start'
},
categoryInner: {
flexWrap: 'wrap',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
flex: 1
},
categoryEmoji: {
color: 'black',
backgroundColor: 'transparent',
textAlign: 'center'
},
customCategoryEmoji: {
margin: 4
}
});

51
app/containers/Header.js Normal file
View File

@ -0,0 +1,51 @@
import React from 'react';
import { View, StyleSheet, Platform } from 'react-native';
import PropTypes from 'prop-types';
import SafeAreaView from 'react-native-safe-area-view';
let platformContainerStyles;
if (Platform.OS === 'ios') {
platformContainerStyles = {
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(0, 0, 0, .3)'
};
} else {
platformContainerStyles = {
shadowColor: 'black',
shadowOpacity: 0.1,
shadowRadius: StyleSheet.hairlineWidth,
shadowOffset: {
height: StyleSheet.hairlineWidth
},
elevation: 4
};
}
const height = Platform.OS === 'ios' ? 44 : 56;
const backgroundColor = Platform.OS === 'ios' ? '#F7F7F7' : '#FFF';
const styles = StyleSheet.create({
container: {
backgroundColor,
...platformContainerStyles
},
appBar: {
height,
backgroundColor
}
});
export default class Header extends React.PureComponent {
static propTypes = {
subview: PropTypes.object.isRequired
}
render() {
return (
<SafeAreaView forceInset={{ bottom: 'never' }} style={styles.container}>
<View style={styles.appBar}>
{this.props.subview}
</View>
</SafeAreaView>
);
}
}

View File

@ -0,0 +1,347 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Clipboard, Vibration, Share } from 'react-native';
import { connect } from 'react-redux';
import ActionSheet from 'react-native-actionsheet';
import * as moment from 'moment';
import {
deleteRequest,
editInit,
toggleStarRequest,
permalinkRequest,
permalinkClear,
togglePinRequest,
setInput,
actionsHide,
toggleReactionPicker
} from '../actions/messages';
import { showToast } from '../utils/info';
import RocketChat from '../lib/rocketchat';
@connect(
state => ({
showActions: state.messages.showActions,
actionMessage: state.messages.actionMessage,
user: state.login.user,
permissions: state.permissions,
permalink: state.messages.permalink,
Message_AllowDeleting: state.settings.Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing: state.settings.Message_AllowEditing,
Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes,
Message_AllowPinning: state.settings.Message_AllowPinning,
Message_AllowStarring: state.settings.Message_AllowStarring
}),
dispatch => ({
actionsHide: () => dispatch(actionsHide()),
deleteRequest: message => dispatch(deleteRequest(message)),
editInit: message => dispatch(editInit(message)),
toggleStarRequest: message => dispatch(toggleStarRequest(message)),
permalinkRequest: message => dispatch(permalinkRequest(message)),
permalinkClear: () => dispatch(permalinkClear()),
togglePinRequest: message => dispatch(togglePinRequest(message)),
setInput: message => dispatch(setInput(message)),
toggleReactionPicker: message => dispatch(toggleReactionPicker(message))
})
)
export default class MessageActions extends React.Component {
static propTypes = {
actionsHide: PropTypes.func.isRequired,
showActions: PropTypes.bool.isRequired,
room: PropTypes.object,
actionMessage: PropTypes.object,
user: PropTypes.object,
permissions: PropTypes.object.isRequired,
deleteRequest: PropTypes.func.isRequired,
editInit: PropTypes.func.isRequired,
toggleStarRequest: PropTypes.func.isRequired,
permalinkRequest: PropTypes.func.isRequired,
permalinkClear: PropTypes.func.isRequired,
togglePinRequest: PropTypes.func.isRequired,
setInput: PropTypes.func.isRequired,
permalink: PropTypes.string,
toggleReactionPicker: PropTypes.func.isRequired,
Message_AllowDeleting: PropTypes.bool,
Message_AllowDeleting_BlockDeleteInMinutes: PropTypes.number,
Message_AllowEditing: PropTypes.bool,
Message_AllowEditing_BlockEditInMinutes: PropTypes.number,
Message_AllowPinning: PropTypes.bool,
Message_AllowStarring: PropTypes.bool
};
constructor(props) {
super(props);
this.state = {
copyPermalink: false,
reply: false,
quote: false
};
this.handleActionPress = this.handleActionPress.bind(this);
this.options = [''];
this.setPermissions(this.props.permissions);
}
async componentWillReceiveProps(nextProps) {
if (nextProps.showActions !== this.props.showActions && nextProps.showActions) {
const { actionMessage } = nextProps;
// Cancel
this.options = ['Cancel'];
this.CANCEL_INDEX = 0;
// Reply
if (!this.isRoomReadOnly()) {
this.options.push('Reply');
this.REPLY_INDEX = this.options.length - 1;
}
// Edit
if (this.allowEdit(nextProps)) {
this.options.push('Edit');
this.EDIT_INDEX = this.options.length - 1;
}
// Permalink
this.options.push('Copy Permalink');
this.PERMALINK_INDEX = this.options.length - 1;
// Copy
this.options.push('Copy Message');
this.COPY_INDEX = this.options.length - 1;
// Share
this.options.push('Share Message');
this.SHARE_INDEX = this.options.length - 1;
// Quote
if (!this.isRoomReadOnly()) {
this.options.push('Quote');
this.QUOTE_INDEX = this.options.length - 1;
}
// Star
if (this.props.Message_AllowStarring) {
this.options.push(actionMessage.starred ? 'Unstar' : 'Star');
this.STAR_INDEX = this.options.length - 1;
}
// Pin
if (this.props.Message_AllowPinning) {
this.options.push(actionMessage.pinned ? 'Unpin' : 'Pin');
this.PIN_INDEX = this.options.length - 1;
}
// Reaction
if (!this.isRoomReadOnly() || this.canReactWhenReadOnly()) {
this.options.push('Add Reaction');
this.REACTION_INDEX = this.options.length - 1;
}
// Delete
if (this.allowDelete(nextProps)) {
this.options.push('Delete');
this.DELETE_INDEX = this.options.length - 1;
}
setTimeout(() => {
this.ActionSheet.show();
Vibration.vibrate(50);
});
} else if (this.props.permalink !== nextProps.permalink && nextProps.permalink) {
// copy permalink
if (this.state.copyPermalink) {
this.setState({ copyPermalink: false });
await Clipboard.setString(nextProps.permalink);
showToast('Permalink copied to clipboard!');
this.props.permalinkClear();
// quote
} else if (this.state.quote) {
this.setState({ quote: false });
const msg = `[ ](${ nextProps.permalink }) `;
this.props.setInput({ msg });
// reply
} else if (this.state.reply) {
this.setState({ reply: false });
let msg = `[ ](${ nextProps.permalink }) `;
// if original message wasn't sent by current user and neither from a direct room
if (this.props.user.username !== this.props.actionMessage.u.username && this.props.room.t !== 'd') {
msg += `@${ this.props.actionMessage.u.username } `;
}
this.props.setInput({ msg });
}
}
}
componentDidUpdate() {
this.setPermissions(this.props.permissions);
}
setPermissions() {
const permissions = ['edit-message', 'delete-message', 'force-delete-message'];
const result = RocketChat.hasPermission(permissions, this.props.room.rid);
this.hasEditPermission = result[permissions[0]];
this.hasDeletePermission = result[permissions[1]];
this.hasForceDeletePermission = result[permissions[2]];
}
isOwn = props => props.actionMessage.u && props.actionMessage.u._id === props.user.id;
isRoomReadOnly = () => this.props.room.ro;
canReactWhenReadOnly = () => this.props.room.reactWhenReadOnly;
allowEdit = (props) => {
if (this.isRoomReadOnly()) {
return false;
}
const editOwn = this.isOwn(props);
const { Message_AllowEditing: isEditAllowed } = this.props;
if (!(this.hasEditPermission || (isEditAllowed && editOwn))) {
return false;
}
const blockEditInMinutes = this.props.Message_AllowEditing_BlockEditInMinutes;
if (blockEditInMinutes) {
let msgTs;
if (props.actionMessage.ts != null) {
msgTs = moment(props.actionMessage.ts);
}
let currentTsDiff;
if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes');
}
return currentTsDiff < blockEditInMinutes;
}
return true;
}
allowDelete = (props) => {
if (this.isRoomReadOnly()) {
return false;
}
const deleteOwn = this.isOwn(props);
const { Message_AllowDeleting: isDeleteAllowed } = this.props;
if (!(this.hasDeletePermission || (isDeleteAllowed && deleteOwn) || this.hasForceDeletePermission)) {
return false;
}
if (this.hasForceDeletePermission) {
return true;
}
const blockDeleteInMinutes = this.props.Message_AllowDeleting_BlockDeleteInMinutes;
if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) {
let msgTs;
if (props.actionMessage.ts != null) {
msgTs = moment(props.actionMessage.ts);
}
let currentTsDiff;
if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes');
}
return currentTsDiff < blockDeleteInMinutes;
}
return true;
}
handleDelete() {
Alert.alert(
'Are you sure?',
'You will not be able to recover this message!',
[
{
text: 'Cancel',
style: 'cancel'
},
{
text: 'Yes, delete it!',
style: 'destructive',
onPress: () => this.props.deleteRequest(this.props.actionMessage)
}
],
{ cancelable: false }
);
}
handleEdit() {
const { _id, msg, rid } = this.props.actionMessage;
this.props.editInit({ _id, msg, rid });
}
handleCopy = async() => {
await Clipboard.setString(this.props.actionMessage.msg);
showToast('Copied to clipboard!');
}
handleShare = async() => {
Share.share({
message: this.props.actionMessage.msg.content.replace(/<(?:.|\n)*?>/gm, '')
});
};
handleStar() {
this.props.toggleStarRequest(this.props.actionMessage);
}
handlePermalink() {
this.setState({ copyPermalink: true });
this.props.permalinkRequest(this.props.actionMessage);
}
handlePin() {
this.props.togglePinRequest(this.props.actionMessage);
}
handleReply() {
this.setState({ reply: true });
this.props.permalinkRequest(this.props.actionMessage);
}
handleQuote() {
this.setState({ quote: true });
this.props.permalinkRequest(this.props.actionMessage);
}
handleReaction() {
this.props.toggleReactionPicker(this.props.actionMessage);
}
handleActionPress = (actionIndex) => {
switch (actionIndex) {
case this.REPLY_INDEX:
this.handleReply();
break;
case this.EDIT_INDEX:
this.handleEdit();
break;
case this.PERMALINK_INDEX:
this.handlePermalink();
break;
case this.COPY_INDEX:
this.handleCopy();
break;
case this.SHARE_INDEX:
this.handleShare();
break;
case this.QUOTE_INDEX:
this.handleQuote();
break;
case this.STAR_INDEX:
this.handleStar();
break;
case this.PIN_INDEX:
this.handlePin();
break;
case this.REACTION_INDEX:
this.handleReaction();
break;
case this.DELETE_INDEX:
this.handleDelete();
break;
default:
break;
}
this.props.actionsHide();
}
render() {
return (
<ActionSheet
ref={o => this.ActionSheet = o}
title='Messages actions'
options={this.options}
cancelButtonIndex={this.CANCEL_INDEX}
destructiveButtonIndex={this.DELETE_INDEX}
onPress={this.handleActionPress}
/>
);
}
}

View File

@ -0,0 +1,23 @@
import React from 'react';
import { View } from 'react-native';
import { KeyboardRegistry } from 'react-native-keyboard-input';
import { Provider } from 'react-redux';
import store from '../../lib/createStore';
import EmojiPicker from '../EmojiPicker';
import styles from './styles';
export default class EmojiKeyboard extends React.PureComponent {
onEmojiSelected = (emoji) => {
KeyboardRegistry.onItemSelected('EmojiKeyboard', { emoji });
}
render() {
return (
<Provider store={store}>
<View style={styles.emojiKeyboardContainer}>
<EmojiPicker onEmojiSelected={emoji => this.onEmojiSelected(emoji)} />
</View>
</Provider>
);
}
}
KeyboardRegistry.registerKeyboard('EmojiKeyboard', () => EmojiKeyboard);

View File

@ -0,0 +1,127 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, SafeAreaView, Platform, PermissionsAndroid, Text } from 'react-native';
import { AudioRecorder, AudioUtils } from 'react-native-audio';
import Icon from 'react-native-vector-icons/MaterialIcons';
import styles from './styles';
export const _formatTime = function(seconds) {
let minutes = Math.floor(seconds / 60);
seconds %= 60;
if (minutes < 10) { minutes = `0${ minutes }`; }
if (seconds < 10) { seconds = `0${ seconds }`; }
return `${ minutes }:${ seconds }`;
};
export default class extends React.PureComponent {
static propTypes = {
onFinish: PropTypes.func.isRequired
}
static async permission() {
if (Platform.OS !== 'android') {
return true;
}
const rationale = {
title: 'Microphone Permission',
message: 'Rocket Chat needs access to your microphone so you can send audio message.'
};
const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, rationale);
return result === true || result === PermissionsAndroid.RESULTS.GRANTED;
}
constructor() {
super();
this.recordingCanceled = false;
this.state = {
currentTime: '00:00'
};
}
componentDidMount() {
const audioPath = `${ AudioUtils.CachesDirectoryPath }/${ Date.now() }.aac`;
AudioRecorder.prepareRecordingAtPath(audioPath, {
SampleRate: 22050,
Channels: 1,
AudioQuality: 'Low',
AudioEncoding: 'aac'
});
AudioRecorder.onProgress = (data) => {
this.setState({
currentTime: _formatTime(Math.floor(data.currentTime))
});
};
//
AudioRecorder.onFinished = (data) => {
if (!this.recordingCanceled && Platform.OS === 'ios') {
this._finishRecording(data.status === 'OK', data.audioFileURL);
}
};
AudioRecorder.startRecording();
}
_finishRecording(didSucceed, filePath) {
if (!didSucceed) {
return this.props.onFinish && this.props.onFinish(didSucceed);
}
const path = filePath.startsWith('file://') ? filePath.split('file://')[1] : filePath;
const fileInfo = {
type: 'audio/aac',
store: 'Uploads',
path
};
return this.props.onFinish && this.props.onFinish(fileInfo);
}
finishAudioMessage = async() => {
try {
const filePath = await AudioRecorder.stopRecording();
if (Platform.OS === 'android') {
this._finishRecording(true, filePath);
}
} catch (err) {
this._finishRecording(false);
console.error(err);
}
}
cancelAudioMessage = async() => {
this.recordingCanceled = true;
await AudioRecorder.stopRecording();
return this._finishRecording(false);
}
render() {
return (
<SafeAreaView
key='messagebox'
style={styles.textBox}
>
<View style={[styles.textArea, { backgroundColor: '#F6F7F9' }]}>
<Icon
style={[styles.actionButtons, { color: 'red' }]}
name='clear'
key='clear'
accessibilityLabel='Cancel recording'
accessibilityTraits='button'
onPress={this.cancelAudioMessage}
/>
<Text key='currentTime' style={[styles.textBoxInput, { width: 50, height: 60 }]}>{this.state.currentTime}</Text>
<Icon
style={[styles.actionButtons, { color: 'green' }]}
name='check'
key='check'
accessibilityLabel='Finish recording'
accessibilityTraits='button'
onPress={this.finishAudioMessage}
/>
</View>
</SafeAreaView>);
}
}

View File

@ -0,0 +1,523 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, TextInput, FlatList, Text, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import ImagePicker from 'react-native-image-picker';
import { connect } from 'react-redux';
import { emojify } from 'react-emojione';
import { KeyboardAccessoryView } from 'react-native-keyboard-input';
import { isIphoneX } from 'react-native-iphone-x-helper';
import { userTyping, layoutAnimation } from '../../actions/room';
import RocketChat from '../../lib/rocketchat';
import { editRequest, editCancel, clearInput } from '../../actions/messages';
import styles from './styles';
import MyIcon from '../icons';
import database from '../../lib/realm';
import Avatar from '../Avatar';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
import { emojis } from '../../emojis';
import Recording from './Recording';
import './EmojiKeyboard';
const MENTIONS_TRACKING_TYPE_USERS = '@';
const MENTIONS_TRACKING_TYPE_EMOJIS = ':';
const onlyUnique = function onlyUnique(value, index, self) {
return self.indexOf(({ _id }) => value._id === _id) === index;
};
@connect(state => ({
room: state.room,
message: state.messages.message,
editing: state.messages.editing,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}), dispatch => ({
editCancel: () => dispatch(editCancel()),
editRequest: message => dispatch(editRequest(message)),
typing: status => dispatch(userTyping(status)),
clearInput: () => dispatch(clearInput()),
layoutAnimation: () => dispatch(layoutAnimation())
}))
export default class MessageBox extends React.PureComponent {
static propTypes = {
onSubmit: PropTypes.func.isRequired,
rid: PropTypes.string.isRequired,
editCancel: PropTypes.func.isRequired,
editRequest: PropTypes.func.isRequired,
baseUrl: PropTypes.string.isRequired,
message: PropTypes.object,
editing: PropTypes.bool,
typing: PropTypes.func,
clearInput: PropTypes.func,
layoutAnimation: PropTypes.func
}
constructor(props) {
super(props);
this.state = {
text: '',
mentions: [],
showMentionsContainer: false,
showEmojiKeyboard: false,
recording: false
};
this.users = [];
this.rooms = [];
this.emojis = [];
this.customEmojis = [];
this._onEmojiSelected = this._onEmojiSelected.bind(this);
}
componentWillReceiveProps(nextProps) {
if (this.props.message !== nextProps.message && nextProps.message.msg) {
this.setState({ text: nextProps.message.msg });
this.component.focus();
} else if (!nextProps.message) {
this.setState({ text: '' });
}
}
onChangeText(text) {
this.setState({ text });
requestAnimationFrame(() => {
const { start, end } = this.component._lastNativeSelection;
const cursor = Math.max(start, end);
const lastNativeText = this.component._lastNativeText;
const regexp = /(#|@|:)([a-z0-9._-]+)$/im;
const result = lastNativeText.substr(0, cursor).match(regexp);
if (!result) {
return this.stopTrackingMention();
}
const [, lastChar, name] = result;
this.identifyMentionKeyword(name, lastChar);
});
}
onKeyboardResigned() {
this.closeEmoji();
}
get leftButtons() {
const { editing } = this.props;
if (editing) {
return (<Icon
style={styles.actionButtons}
name='close'
accessibilityLabel='Cancel editing'
accessibilityTraits='button'
onPress={() => this.editCancel()}
/>);
}
return !this.state.showEmojiKeyboard ? (<Icon
style={styles.actionButtons}
onPress={() => this.openEmoji()}
accessibilityLabel='Open emoji selector'
accessibilityTraits='button'
name='mood'
/>) : (<Icon
onPress={() => this.closeEmoji()}
style={styles.actionButtons}
accessibilityLabel='Close emoji selector'
accessibilityTraits='button'
name='keyboard'
/>);
}
get rightButtons() {
const icons = [];
if (this.state.text) {
icons.push(<MyIcon
style={[styles.actionButtons, { color: '#1D74F5' }]}
name='send'
key='sendIcon'
accessibilityLabel='Send message'
accessibilityTraits='button'
onPress={() => this.submit(this.state.text)}
/>);
return icons;
}
icons.push(<Icon
style={[styles.actionButtons, { color: '#1D74F5', paddingHorizontal: 10 }]}
name='mic'
key='micIcon'
accessibilityLabel='Send audio message'
accessibilityTraits='button'
onPress={() => this.recordAudioMessage()}
/>);
icons.push(<MyIcon
style={[styles.actionButtons, { color: '#2F343D', fontSize: 16 }]}
name='plus'
key='fileIcon'
accessibilityLabel='Message actions'
accessibilityTraits='button'
onPress={() => this.addFile()}
/>);
return icons;
}
addFile = () => {
const options = {
maxHeight: 1960,
maxWidth: 1960,
quality: 0.8,
customButtons: [{
name: 'import', title: 'Import File From'
}]
};
ImagePicker.showImagePicker(options, (response) => {
if (response.didCancel) {
console.log('User cancelled image picker');
} else if (response.error) {
console.log('ImagePicker Error: ', response.error);
} else if (response.customButton) {
console.log('User tapped custom button: ', response.customButton);
} else {
const fileInfo = {
name: response.fileName,
size: response.fileSize,
type: response.type || 'image/jpeg',
// description: '',
store: 'Uploads'
};
RocketChat.sendFileMessage(this.props.rid, fileInfo, response.data);
}
});
}
editCancel() {
this.props.editCancel();
this.setState({ text: '' });
}
async openEmoji() {
await this.setState({
showEmojiKeyboard: true
});
}
async recordAudioMessage() {
const recording = await Recording.permission();
this.setState({ recording });
}
finishAudioMessage = async(fileInfo) => {
if (fileInfo) {
RocketChat.sendFileMessage(this.props.rid, fileInfo);
}
this.setState({
recording: false
});
}
closeEmoji() {
this.setState({ showEmojiKeyboard: false });
}
submit(message) {
this.setState({ text: '' });
this.closeEmoji();
this.stopTrackingMention();
this.props.typing(false);
if (message.trim() === '') {
return;
}
// if is editing a message
const { editing } = this.props;
if (editing) {
const { _id, rid } = this.props.message;
this.props.editRequest({ _id, msg: message, rid });
} else {
// if is submiting a new message
this.props.onSubmit(message);
}
this.props.clearInput();
}
_getFixedMentions(keyword) {
if ('all'.indexOf(keyword) !== -1) {
this.users = [{ _id: -1, username: 'all', desc: 'all' }, ...this.users];
}
if ('here'.indexOf(keyword) !== -1) {
this.users = [{ _id: -2, username: 'here', desc: 'active users' }, ...this.users];
}
}
async _getUsers(keyword) {
this.users = database.objects('users');
if (keyword) {
this.users = this.users.filtered('username CONTAINS[c] $0', keyword);
}
this._getFixedMentions(keyword);
this.setState({ mentions: this.users.slice() });
const usernames = [];
if (keyword && this.users.length > 7) {
return;
}
this.users.forEach(user => usernames.push(user.username));
if (this.oldPromise) {
this.oldPromise();
}
try {
const results = await Promise.race([
RocketChat.spotlight(keyword, usernames, { users: true }),
new Promise((resolve, reject) => (this.oldPromise = reject))
]);
database.write(() => {
results.users.forEach((user) => {
database.create('users', user, true);
});
});
} catch (e) {
console.log('spotlight canceled');
} finally {
delete this.oldPromise;
this.users = database.objects('users').filtered('username CONTAINS[c] $0', keyword).slice();
this._getFixedMentions(keyword);
this.setState({ mentions: this.users });
}
}
async _getRooms(keyword = '') {
this.roomsCache = this.roomsCache || [];
this.rooms = database.objects('subscriptions')
.filtered('t != $0', 'd');
if (keyword) {
this.rooms = this.rooms.filtered('name CONTAINS[c] $0', keyword);
}
const rooms = [];
this.rooms.forEach(room => rooms.push(room));
this.roomsCache.forEach((room) => {
if (room.name && room.name.toUpperCase().indexOf(keyword.toUpperCase()) !== -1) {
rooms.push(room);
}
});
if (rooms.length > 3) {
this.setState({ mentions: rooms });
return;
}
if (this.oldPromise) {
this.oldPromise();
}
try {
const results = await Promise.race([
RocketChat.spotlight(keyword, [...rooms, ...this.roomsCache].map(r => r.name), { rooms: true }),
new Promise((resolve, reject) => (this.oldPromise = reject))
]);
this.roomsCache = [...this.roomsCache, ...results.rooms].filter(onlyUnique);
this.setState({ mentions: [...rooms.slice(), ...results.rooms] });
} catch (e) {
console.log('spotlight canceled');
} finally {
delete this.oldPromise;
}
}
_getEmojis(keyword) {
if (keyword) {
this.customEmojis = database.objects('customEmojis').filtered('name CONTAINS[c] $0', keyword).slice(0, 4);
this.emojis = emojis.filter(emoji => emoji.indexOf(keyword) !== -1).slice(0, 4);
const mergedEmojis = [...this.customEmojis, ...this.emojis];
this.setState({ mentions: mergedEmojis });
}
}
stopTrackingMention() {
this.setState({
showMentionsContainer: false,
mentions: [],
trackingType: ''
});
this.users = [];
this.rooms = [];
this.customEmojis = [];
this.emojis = [];
}
identifyMentionKeyword(keyword, type) {
if (!this.state.showMentionsContainer) {
this.props.layoutAnimation();
}
this.setState({
showMentionsContainer: true,
showEmojiKeyboard: false,
trackingType: type
});
this.updateMentions(keyword, type);
}
updateMentions = (keyword, type) => {
if (type === MENTIONS_TRACKING_TYPE_USERS) {
this._getUsers(keyword);
} else if (type === MENTIONS_TRACKING_TYPE_EMOJIS) {
this._getEmojis(keyword);
} else {
this._getRooms(keyword);
}
}
_onPressMention(item) {
const msg = this.component._lastNativeText;
const { start, end } = this.component._lastNativeSelection;
const cursor = Math.max(start, end);
const regexp = /([a-z0-9._-]+)$/im;
const result = msg.substr(0, cursor).replace(regexp, '');
const mentionName = this.state.trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ?
`${ item.name || item }:` : (item.username || item.name);
const text = `${ result }${ mentionName } ${ msg.slice(cursor) }`;
this.component.setNativeProps({ text });
this.setState({ text });
this.component.focus();
requestAnimationFrame(() => this.stopTrackingMention());
}
_onEmojiSelected(keyboardId, params) {
const { text } = this.state;
const { emoji } = params;
let newText = '';
// if messagebox has an active cursor
if (this.component._lastNativeSelection) {
const { start, end } = this.component._lastNativeSelection;
const cursor = Math.max(start, end);
newText = `${ text.substr(0, cursor) }${ emoji }${ text.substr(cursor) }`;
} else {
// if messagebox doesn't have a cursor, just append selected emoji
newText = `${ text }${ emoji }`;
}
this.component.setNativeProps({ text: newText });
this.setState({ text: newText });
}
renderFixedMentionItem = item => (
<TouchableOpacity
style={styles.mentionItem}
onPress={() => this._onPressMention(item)}
>
<Text style={styles.fixedMentionAvatar}>{item.username}</Text>
<Text>Notify {item.desc} in this room</Text>
</TouchableOpacity>
)
renderMentionEmoji = (item) => {
if (item.name) {
return (
<CustomEmoji
key='mention-item-avatar'
style={styles.mentionItemCustomEmoji}
emoji={item}
baseUrl={this.props.baseUrl}
/>
);
}
return (
<Text
key='mention-item-avatar'
style={styles.mentionItemEmoji}
>
{emojify(`:${ item }:`, { output: 'unicode' })}
</Text>
);
}
renderMentionItem = (item) => {
if (item.username === 'all' || item.username === 'here') {
return this.renderFixedMentionItem(item);
}
return (
<TouchableOpacity
style={styles.mentionItem}
onPress={() => this._onPressMention(item)}
>
{this.state.trackingType === MENTIONS_TRACKING_TYPE_EMOJIS ?
[
this.renderMentionEmoji(item),
<Text key='mention-item-name'>:{ item.name || item }:</Text>
]
: [
<Avatar
key='mention-item-avatar'
style={{ margin: 8 }}
text={item.username || item.name}
size={30}
baseUrl={this.props.baseUrl}
/>,
<Text key='mention-item-name'>{ item.username || item.name }</Text>
]
}
</TouchableOpacity>
);
}
renderMentions = () => (
<FlatList
key='messagebox-container'
style={styles.mentionList}
data={this.state.mentions}
renderItem={({ item }) => this.renderMentionItem(item)}
keyExtractor={item => item._id || item}
keyboardShouldPersistTaps='always'
/>
);
renderContent() {
if (this.state.recording) {
return (<Recording onFinish={this.finishAudioMessage} />);
}
return (
[
this.renderMentions(),
<View key='messagebox' style={[styles.textArea, this.props.editing && styles.editing]}>
{this.leftButtons}
<TextInput
ref={component => this.component = component}
style={styles.textBoxInput}
returnKeyType='default'
keyboardType='twitter'
blurOnSubmit={false}
placeholder='New Message'
onChangeText={text => this.onChangeText(text)}
value={this.state.text}
underlineColorAndroid='transparent'
defaultValue=''
multiline
placeholderTextColor='#9EA2A8'
/>
{this.rightButtons}
</View>
]
);
}
render() {
return (
[
<KeyboardAccessoryView
key='input'
renderContent={() => this.renderContent()}
kbInputRef={this.component}
kbComponent={this.state.showEmojiKeyboard ? 'EmojiKeyboard' : null}
onKeyboardResigned={() => this.onKeyboardResigned()}
onItemSelected={this._onEmojiSelected}
trackInteractive
// revealKeyboardInteractive
requiresSameParentToManageScrollView
/>,
isIphoneX() ? <View key='iphonex-area' style={styles.iphoneXArea} /> : null
]
);
}
}

View File

@ -0,0 +1,85 @@
import { StyleSheet, Platform } from 'react-native';
const MENTION_HEIGHT = 50;
export default StyleSheet.create({
textBox: {
backgroundColor: '#fff',
flex: 0,
alignItems: 'center',
borderTopWidth: 1,
borderTopColor: '#D8D8D8',
zIndex: 2
},
textArea: {
flexDirection: 'row',
alignItems: 'center',
flexGrow: 0,
backgroundColor: '#fff'
},
textBoxInput: {
textAlignVertical: 'center',
maxHeight: 120,
flexGrow: 1,
width: 1,
paddingTop: 15,
paddingBottom: 15,
paddingLeft: 0,
paddingRight: 0
},
editing: {
backgroundColor: '#fff5df'
},
actionButtons: {
color: '#2F343D',
fontSize: 20,
textAlign: 'center',
padding: 15,
paddingHorizontal: 21,
flex: 0
},
mentionList: {
maxHeight: MENTION_HEIGHT * 4,
borderTopColor: '#ECECEC',
borderTopWidth: 1,
paddingHorizontal: 5,
backgroundColor: '#fff'
},
mentionItem: {
height: MENTION_HEIGHT,
backgroundColor: '#F7F8FA',
borderBottomWidth: 1,
borderBottomColor: '#ECECEC',
flexDirection: 'row',
alignItems: 'center'
},
mentionItemCustomEmoji: {
margin: 8,
width: 30,
height: 30
},
mentionItemEmoji: {
width: 46,
height: 36,
fontSize: Platform.OS === 'ios' ? 30 : 25,
textAlign: 'center'
},
fixedMentionAvatar: {
fontWeight: 'bold',
textAlign: 'center',
width: 46
},
emojiKeyboardContainer: {
flex: 1,
borderTopColor: '#ECECEC',
borderTopWidth: 1
},
iphoneXArea: {
height: 50,
backgroundColor: '#fff',
position: 'absolute',
bottom: 0,
left: 0,
right: 0
}
});

View File

@ -0,0 +1,76 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import ActionSheet from 'react-native-actionsheet';
import { errorActionsHide } from '../actions/messages';
import RocketChat from '../lib/rocketchat';
import database from '../lib/realm';
@connect(
state => ({
showErrorActions: state.messages.showErrorActions,
actionMessage: state.messages.actionMessage
}),
dispatch => ({
errorActionsHide: () => dispatch(errorActionsHide())
})
)
export default class MessageErrorActions extends React.Component {
static propTypes = {
errorActionsHide: PropTypes.func.isRequired,
showErrorActions: PropTypes.bool.isRequired,
actionMessage: PropTypes.object
};
constructor(props) {
super(props);
this.handleActionPress = this.handleActionPress.bind(this);
this.options = ['Cancel', 'Delete', 'Resend'];
this.CANCEL_INDEX = 0;
this.DELETE_INDEX = 1;
this.RESEND_INDEX = 2;
}
componentWillReceiveProps(nextProps) {
if (nextProps.showErrorActions !== this.props.showErrorActions && nextProps.showErrorActions) {
this.ActionSheet.show();
}
}
handleResend = () => RocketChat.resendMessage(this.props.actionMessage._id);
handleDelete = () => {
database.write(() => {
const msg = database.objects('messages').filtered('_id = $0', this.props.actionMessage._id);
database.delete(msg);
});
}
handleActionPress = (actionIndex) => {
switch (actionIndex) {
case this.RESEND_INDEX:
this.handleResend();
break;
case this.DELETE_INDEX:
this.handleDelete();
break;
default:
break;
}
this.props.errorActionsHide();
}
render() {
return (
<ActionSheet
ref={o => this.ActionSheet = o}
title='Messages actions'
options={this.options}
cancelButtonIndex={this.CANCEL_INDEX}
destructiveButtonIndex={this.DELETE_INDEX}
onPress={this.handleActionPress}
/>
);
}
}

58
app/containers/Routes.js Normal file
View File

@ -0,0 +1,58 @@
import PropTypes from 'prop-types';
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import SplashScreen from 'react-native-splash-screen';
import { appInit } from '../actions';
import AuthRoutes from './routes/AuthRoutes';
import PublicRoutes from './routes/PublicRoutes';
import * as NavigationService from './routes/NavigationService';
@connect(
state => ({
login: state.login,
app: state.app,
background: state.app.background
}),
dispatch => bindActionCreators({
appInit
}, dispatch)
)
export default class Routes extends React.Component {
static propTypes = {
login: PropTypes.object.isRequired,
app: PropTypes.object.isRequired,
appInit: PropTypes.func.isRequired
}
componentDidMount() {
if (this.props.app.ready) {
return SplashScreen.hide();
}
this.props.appInit();
}
componentWillReceiveProps(nextProps) {
if (nextProps.app.ready && this.props.app.ready !== nextProps.app.ready) {
SplashScreen.hide();
}
}
componentDidUpdate() {
NavigationService.setNavigator(this.navigator);
}
render() {
const { login } = this.props;
if (this.props.app.starting) {
return null;
}
if (!login.token || login.isRegistering) {
return (<PublicRoutes ref={nav => this.navigator = nav} />);
}
return (<AuthRoutes ref={nav => this.navigator = nav} />);
}
}

135
app/containers/Sidebar.js Normal file
View File

@ -0,0 +1,135 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { ScrollView, Text, View, StyleSheet, FlatList, TouchableHighlight } from 'react-native';
import { connect } from 'react-redux';
import database from '../lib/realm';
import { setServer, gotoAddServer } from '../actions/server';
import { logout } from '../actions/login';
const styles = StyleSheet.create({
scrollView: {
paddingTop: 20
},
imageContainer: {
width: '100%',
alignItems: 'center'
},
image: {
width: 200,
height: 200,
borderRadius: 100
},
serverTitle: {
fontSize: 16,
color: 'grey',
padding: 10,
width: '100%'
},
serverItem: {
backgroundColor: 'white',
padding: 10,
flex: 1
},
selectedServer: {
backgroundColor: '#eeeeee'
}
});
const keyExtractor = item => item.id;
@connect(state => ({
server: state.server.server
}), dispatch => ({
selectServer: server => dispatch(setServer(server)),
logout: () => dispatch(logout()),
gotoAddServer: () => dispatch(gotoAddServer())
}))
export default class Sidebar extends Component {
static propTypes = {
server: PropTypes.string.isRequired,
selectServer: PropTypes.func.isRequired,
navigation: PropTypes.object.isRequired,
logout: PropTypes.func.isRequired,
gotoAddServer: PropTypes.func.isRequired
}
constructor(props) {
super(props);
this.state = { servers: [] };
}
componentDidMount() {
database.databases.serversDB.addListener('change', this.updateState);
this.setState(this.getState());
}
componentWillUnmount() {
database.databases.serversDB.removeListener('change', this.updateState);
}
onItemPress = ({ route, focused }) => {
this.props.navigation.navigate({ key: 'DrawerClose', routeName: 'DrawerClose' });
if (!focused) {
this.props.navigation.navigate(route.routeName, undefined);
}
}
onPressItem = (item) => {
this.props.selectServer(item.id);
this.props.navigation.navigate({ key: 'DrawerClose', routeName: 'DrawerClose' });
}
getState = () => ({
servers: database.databases.serversDB.objects('servers')
})
updateState = () => {
this.setState(this.getState());
}
renderItem = ({ item, separators }) => (
<TouchableHighlight
onShowUnderlay={separators.highlight}
onHideUnderlay={separators.unhighlight}
onPress={() => { this.onPressItem(item); }}
>
<View style={[styles.serverItem, (item.id === this.props.server ? styles.selectedServer : null)]}>
<Text>
{item.id}
</Text>
</View>
</TouchableHighlight>
);
render() {
return (
<ScrollView style={styles.scrollView}>
<View style={{ paddingBottom: 20 }}>
<FlatList
data={this.state.servers}
renderItem={this.renderItem}
keyExtractor={keyExtractor}
/>
<TouchableHighlight
onPress={() => { this.props.logout(); }}
>
<View style={styles.serverItem}>
<Text>
Logout
</Text>
</View>
</TouchableHighlight>
<TouchableHighlight
onPress={() => { this.props.gotoAddServer(); }}
>
<View style={styles.serverItem}>
<Text>
Add Server
</Text>
</View>
</TouchableHighlight>
</View>
</ScrollView>
);
}
}

View File

@ -0,0 +1,88 @@
import React from 'react';
import { View, StyleSheet, Text, TextInput } from 'react-native';
import PropTypes from 'prop-types';
import Icon from 'react-native-vector-icons/FontAwesome';
import sharedStyles from '../views/Styles';
import { COLOR_DANGER } from '../constants/colors';
const styles = StyleSheet.create({
inputContainer: {
marginBottom: 20
},
label: {
marginBottom: 4,
fontSize: 16
},
input: {
paddingTop: 12,
paddingBottom: 12,
paddingHorizontal: 10,
borderWidth: 2,
borderRadius: 2,
backgroundColor: 'white',
borderColor: 'rgba(0,0,0,.15)',
color: 'black'
},
labelError: {
color: COLOR_DANGER
},
inputError: {
color: COLOR_DANGER,
borderColor: COLOR_DANGER
},
wrap: {
flex: 1,
position: 'relative'
},
icon: {
position: 'absolute',
right: 0,
padding: 10,
color: 'rgba(0,0,0,.45)'
}
});
export default class RCTextInput extends React.PureComponent {
static propTypes = {
label: PropTypes.string,
error: PropTypes.object,
secureTextEntry: PropTypes.bool
}
static defaultProps = {
error: {}
}
state = {
showPassword: false
}
get icon() { return <Icon name={this.state.showPassword ? 'eye-slash' : 'eye'} style={styles.icon} size={20} onPress={this.tooglePassword} />; }
tooglePassword = () => this.setState({ showPassword: !this.state.showPassword })
render() {
const {
label, error, secureTextEntry, ...inputProps
} = this.props;
const { showPassword } = this.state;
return (
<View style={styles.inputContainer}>
{ label && <Text style={[styles.label, error.error && styles.labelError]}>{label}</Text> }
<View style={styles.wrap}>
<TextInput
style={[styles.input, error.error && styles.inputError]}
autoCorrect={false}
autoCapitalize='none'
underlineColorAndroid='transparent'
secureTextEntry={secureTextEntry && !showPassword}
{...inputProps}
/>
{secureTextEntry && this.icon}
</View>
{error.error && <Text style={sharedStyles.error}>{error.reason}</Text>}
</View>
);
}
}

42
app/containers/Typing.js Normal file
View File

@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StyleSheet, Text, Keyboard } from 'react-native';
import { connect } from 'react-redux';
const styles = StyleSheet.create({
typing: {
transform: [{ scaleY: -1 }],
fontWeight: 'bold',
paddingHorizontal: 15,
height: 25
}
});
@connect(state => ({
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();
}
onPress = () => {
Keyboard.dismiss();
}
get usersTyping() {
const users = this.props.usersTyping.filter(_username => this.props.username !== _username);
return users.length ? `${ users.join(' ,') } ${ users.length > 1 ? 'are' : 'is' } typing` : '';
}
render() {
return (<Text style={styles.typing} onPress={() => this.onPress()}>{this.usersTyping}</Text>);
}
}
Typing.propTypes = {
username: PropTypes.string,
usersTyping: PropTypes.array
};

4
app/containers/icons.js Normal file
View File

@ -0,0 +1,4 @@
import { createIconSetFromIcoMoon } from 'react-native-vector-icons';
import iconConfig from '../icons.json';
export default createIconSetFromIcoMoon(iconConfig);

View File

@ -0,0 +1,162 @@
import React from 'react';
import PropTypes from 'prop-types';
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 Markdown from './Markdown';
const styles = StyleSheet.create({
audioContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
height: 50,
margin: 5,
backgroundColor: '#eee',
borderRadius: 6
},
playPauseButton: {
width: 50,
alignItems: 'center',
backgroundColor: 'transparent',
borderRightColor: '#ccc',
borderRightWidth: 1
},
playPauseIcon: {
color: '#ccc',
backgroundColor: 'transparent'
},
progressContainer: {
flex: 1,
justifyContent: 'center',
height: '100%',
marginHorizontal: 10
},
label: {
color: '#888',
fontSize: 10
},
currentTime: {
position: 'absolute',
left: 0,
bottom: 2
},
duration: {
position: 'absolute',
right: 0,
bottom: 2
}
});
const formatTime = (t = 0, duration = 0) => {
const time = Math.min(
Math.max(t, 0),
duration
);
const formattedMinutes = Math.floor(time / 60).toFixed(0).padStart(2, 0);
const formattedSeconds = Math.floor(time % 60).toFixed(0).padStart(2, 0);
return `${ formattedMinutes }:${ formattedSeconds }`;
};
export default class Audio extends React.PureComponent {
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired
}
constructor(props) {
super(props);
this.onLoad = this.onLoad.bind(this);
this.onProgress = this.onProgress.bind(this);
this.onEnd = this.onEnd.bind(this);
const { baseUrl, file, user } = props;
this.state = {
currentTime: 0,
duration: 0,
paused: true,
uri: `${ baseUrl }${ file.audio_url }?rc_uid=${ user.id }&rc_token=${ user.token }`
};
}
onLoad(data) {
this.setState({ duration: data.duration > 0 ? data.duration : 0 });
}
onProgress(data) {
if (data.currentTime < this.state.duration) {
this.setState({ currentTime: data.currentTime });
}
}
onEnd() {
this.setState({ paused: true, currentTime: 0 });
requestAnimationFrame(() => {
this.player.seek(0);
});
}
getCurrentTime() {
return formatTime(this.state.currentTime, this.state.duration);
}
getDuration() {
return formatTime(this.state.duration);
}
togglePlayPause() {
this.setState({ paused: !this.state.paused });
}
render() {
const { uri, paused } = this.state;
const { description } = this.props.file;
return (
<View>
<View style={styles.audioContainer}>
<Video
ref={(ref) => {
this.player = ref;
}}
source={{ uri }}
onLoad={this.onLoad}
onProgress={this.onProgress}
onEnd={this.onEnd}
paused={paused}
repeat={false}
/>
<TouchableOpacity
style={styles.playPauseButton}
onPress={() => this.togglePlayPause()}
>
{
paused ? <Icon name='play-arrow' size={50} style={styles.playPauseIcon} />
: <Icon name='pause' size={47} style={styles.playPauseIcon} />
}
</TouchableOpacity>
<View style={styles.progressContainer}>
<Text style={[styles.label, styles.currentTime]}>{this.getCurrentTime()}</Text>
<Text style={[styles.label, styles.duration]}>{this.getDuration()}</Text>
<Slider
value={this.state.currentTime}
maximumValue={this.state.duration}
minimumValue={0}
animateTransitions
animationConfig={{
duration: 250,
easing: Easing.linear,
delay: 0
}}
thumbTintColor='#ccc'
onValueChange={value => this.setState({ currentTime: value })}
/>
</View>
</View>
<Markdown msg={description} />
</View>
);
}
}

View File

@ -0,0 +1,26 @@
import React from 'react';
import { Text, ViewPropTypes } from 'react-native';
import PropTypes from 'prop-types';
import { emojify } from 'react-emojione';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
export default class Emoji extends React.PureComponent {
static propTypes = {
content: PropTypes.string,
standardEmojiStyle: Text.propTypes.style,
customEmojiStyle: ViewPropTypes.style,
customEmojis: PropTypes.object.isRequired
};
render() {
const {
content, standardEmojiStyle, customEmojiStyle, customEmojis
} = this.props;
const parsedContent = content.replace(/^:|:$/g, '');
const emojiExtension = customEmojis[parsedContent];
if (emojiExtension) {
const emoji = { extension: emojiExtension, content: parsedContent };
return <CustomEmoji key={content} style={customEmojiStyle} emoji={emoji} />;
}
return <Text style={standardEmojiStyle}>{ emojify(`${ content }`, { output: 'unicode' }) }</Text>;
}
}

View File

@ -0,0 +1,86 @@
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 PhotoModal from './PhotoModal';
const styles = StyleSheet.create({
button: {
flex: 1,
flexDirection: 'column',
height: 320,
borderColor: '#ccc',
borderWidth: 1,
borderRadius: 6
},
image: {
flex: 1,
height: undefined,
width: undefined,
resizeMode: 'contain'
},
labelContainer: {
height: 62,
alignItems: 'center',
justifyContent: 'center'
},
imageName: {
fontSize: 12,
alignSelf: 'center',
fontStyle: 'italic'
},
message: {
alignSelf: 'center',
fontWeight: 'bold'
}
});
export default class Image extends React.PureComponent {
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired
}
state = { modalVisible: false };
getDescription() {
if (this.props.file.description) {
return <Text style={styles.message}>{this.props.file.description}</Text>;
}
}
_onPressButton() {
this.setState({
modalVisible: true
});
}
render() {
const { baseUrl, file, user } = this.props;
const img = `${ baseUrl }${ file.image_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
return (
<View>
<TouchableOpacity
onPress={() => this._onPressButton()}
style={styles.button}
>
<CachedImage
style={styles.image}
source={{ uri: encodeURI(img) }}
/>
<View style={styles.labelContainer}>
<Text style={styles.imageName}>{this.props.file.title}</Text>
{this.getDescription()}
</View>
</TouchableOpacity>
<PhotoModal
title={this.props.file.title}
image={img}
isVisible={this.state.modalVisible}
onClose={() => this.setState({ modalVisible: false })}
/>
</View>
);
}
}

View File

@ -0,0 +1,153 @@
import React from 'react';
import { Text, StyleSheet, ViewPropTypes } from 'react-native';
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 styles from './styles';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
const BlockCode = ({ node, state }) => (
<Text
key={state.key}
style={styles.codeStyle}
>
{node.content}
</Text>
);
const mentionStyle = { color: '#13679a' };
const Markdown = ({
msg, customEmojis, style, markdownStyle, customRules, renderInline
}) => {
if (!msg) {
return null;
}
msg = emojify(msg, { output: 'unicode' });
const defaultRules = {
username: {
order: -1,
match: SimpleMarkdown.inlineRegex(/^@[0-9a-zA-Z-_.]+/),
parse: capture => ({ content: capture[0] }),
react: (node, output, state) => ({
type: 'custom',
key: state.key,
props: {
children: (
<Text
key={state.key}
style={mentionStyle}
onPress={() => alert('Username')}
>
{node.content}
</Text>
)
}
})
},
heading: {
order: -2,
match: SimpleMarkdown.inlineRegex(/^#[0-9a-zA-Z-_.]+/),
parse: capture => ({ content: capture[0] }),
react: (node, output, state) => ({
type: 'custom',
key: state.key,
props: {
children: (
<Text
key={state.key}
style={mentionStyle}
onPress={() => alert('Room')}
>
{node.content}
</Text>
)
}
})
},
fence: {
order: -3,
match: SimpleMarkdown.blockRegex(/^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n *)+\n/),
parse: capture => ({
lang: capture[2] || undefined,
content: capture[3]
}),
react: (node, output, state) => ({
type: 'custom',
key: state.key,
props: {
children: (
<BlockCode key={state.key} node={node} state={state} />
)
}
})
},
blockCode: {
order: -4,
match: SimpleMarkdown.blockRegex(/^(```)\s*([\s\S]*?[^`])\s*\1(?!```)/),
parse: capture => ({ content: capture[2] }),
react: (node, output, state) => ({
type: 'custom',
key: state.key,
props: {
children: (
<BlockCode key={state.key} node={node} state={state} />
)
}
})
},
customEmoji: {
order: -5,
match: SimpleMarkdown.inlineRegex(/^:([0-9a-zA-Z-_.]+):/),
parse: capture => ({ content: capture }),
react: (node, output, state) => {
const element = {
type: 'custom',
key: state.key,
props: {
children: <Text key={state.key}>{node.content[0]}</Text>
}
};
const content = node.content[1];
const emojiExtension = customEmojis[content];
if (emojiExtension) {
const emoji = { extension: emojiExtension, content };
element.props.children = (
<CustomEmoji key={state.key} style={styles.customEmoji} emoji={emoji} />
);
}
return element;
}
}
};
const codeStyle = StyleSheet.flatten(styles.codeStyle);
style = StyleSheet.flatten(style);
return (
<EasyMarkdown
style={{ marginBottom: 0, ...style }}
markdownStyles={{ code: codeStyle, ...markdownStyle }}
rules={{ ...defaultRules, ...customRules }}
renderInline={renderInline}
>{msg}
</EasyMarkdown>
);
};
Markdown.propTypes = {
msg: PropTypes.string.isRequired,
customEmojis: PropTypes.object,
// eslint-disable-next-line react/no-typos
style: ViewPropTypes.style,
markdownStyle: PropTypes.object,
customRules: PropTypes.object,
renderInline: PropTypes.bool
};
BlockCode.propTypes = {
node: PropTypes.object,
state: PropTypes.object
};
export default Markdown;

View File

@ -0,0 +1,65 @@
import React from 'react';
import { ScrollView, View, Text, TouchableWithoutFeedback } from 'react-native';
import { CachedImage } from 'react-native-img-cache';
import PropTypes from 'prop-types';
import Modal from 'react-native-modal';
const styles = {
imageWrapper: {
alignItems: 'stretch',
flex: 1
},
image: {
flex: 1
},
titleContainer: {
width: '100%',
alignItems: 'center',
marginVertical: 10
},
title: {
color: '#ffffff',
textAlign: 'center',
fontSize: 16,
fontWeight: '600'
}
};
export default class PhotoModal extends React.PureComponent {
static propTypes = {
title: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
isVisible: PropTypes.bool,
onClose: PropTypes.func.isRequired
}
render() {
const {
image, isVisible, onClose, title
} = this.props;
return (
<Modal
isVisible={isVisible}
onBackdropPress={onClose}
onBackButtonPress={onClose}
>
<TouchableWithoutFeedback onPress={onClose}>
<View style={styles.titleContainer}>
<Text style={styles.title}>{title}</Text>
</View>
</TouchableWithoutFeedback>
<View style={styles.imageWrapper}>
<ScrollView contentContainerStyle={styles.imageWrapper} maximumZoomScale={5}>
<TouchableWithoutFeedback onPress={onClose}>
<CachedImage
style={styles.image}
source={{ uri: encodeURI(image) }}
mutable
resizeMode='contain'
/>
</TouchableWithoutFeedback>
</ScrollView>
</View>
</Modal>
);
}
}

View File

@ -0,0 +1,20 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
const styles = StyleSheet.create({
quoteSign: {
borderWidth: 2,
borderRadius: 4,
height: '100%',
marginRight: 5
}
});
const QuoteMark = ({ color }) => <View style={[styles.quoteSign, { borderColor: color || '#a0a0a0' }]} />;
QuoteMark.propTypes = {
color: PropTypes.string
};
export default QuoteMark;

View File

@ -0,0 +1,124 @@
import React from 'react';
import { View, Text, TouchableWithoutFeedback, FlatList, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import Modal from 'react-native-modal';
import Icon from 'react-native-vector-icons/MaterialIcons';
import Emoji from './Emoji';
const styles = StyleSheet.create({
titleContainer: {
width: '100%',
alignItems: 'center',
paddingVertical: 10
},
title: {
color: '#ffffff',
textAlign: 'center',
fontSize: 16,
fontWeight: '600'
},
reactCount: {
color: '#dddddd',
fontSize: 10
},
peopleReacted: {
color: '#ffffff',
fontWeight: '500'
},
peopleItemContainer: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center'
},
emojiContainer: {
width: 50,
height: 50,
alignItems: 'center',
justifyContent: 'center'
},
itemContainer: {
height: 50,
flexDirection: 'row'
},
listContainer: {
flex: 1
},
closeButton: {
position: 'absolute',
left: 0,
top: 10,
color: '#ffffff'
}
});
const standardEmojiStyle = { fontSize: 20 };
const customEmojiStyle = { width: 20, height: 20 };
export default class ReactionsModal extends React.PureComponent {
static propTypes = {
isVisible: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
reactions: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
customEmojis: PropTypes.object.isRequired
}
renderItem = (item) => {
const count = item.usernames.length;
let usernames = item.usernames.slice(0, 3)
.map(username => (username.value === this.props.user.username ? 'you' : username.value)).join(', ');
if (count > 3) {
usernames = `${ usernames } and more ${ count - 3 }`;
} else {
usernames = usernames.replace(/,(?=[^,]*$)/, ' and');
}
return (
<View style={styles.itemContainer}>
<View style={styles.emojiContainer}>
<Emoji
content={item.emoji}
standardEmojiStyle={standardEmojiStyle}
customEmojiStyle={customEmojiStyle}
customEmojis={this.props.customEmojis}
/>
</View>
<View style={styles.peopleItemContainer}>
<Text style={styles.reactCount}>
{count === 1 ? '1 person' : `${ count } people`} reacted
</Text>
<Text style={styles.peopleReacted}>{ usernames }</Text>
</View>
</View>
);
}
render() {
const {
isVisible, onClose, reactions
} = this.props;
return (
<Modal
isVisible={isVisible}
onBackdropPress={onClose}
onBackButtonPress={onClose}
backdropOpacity={0.9}
>
<TouchableWithoutFeedback onPress={onClose}>
<View style={styles.titleContainer}>
<Icon
style={styles.closeButton}
name='close'
size={20}
onPress={onClose}
/>
<Text style={styles.title}>Reactions</Text>
</View>
</TouchableWithoutFeedback>
<View style={styles.listContainer}>
<FlatList
data={reactions}
renderItem={({ item }) => this.renderItem(item)}
keyExtractor={item => item.emoji}
/>
</View>
</Modal>
);
}
}

View File

@ -0,0 +1,157 @@
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import moment from 'moment';
import Markdown from './Markdown';
import QuoteMark from './QuoteMark';
import Avatar from '../Avatar';
import openLink from '../../utils/openLink';
const styles = StyleSheet.create({
button: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginTop: 2,
alignSelf: 'flex-end'
},
quoteSign: {
borderWidth: 2,
borderRadius: 4,
borderColor: '#a0a0a0',
height: '100%',
marginRight: 5
},
attachmentContainer: {
flex: 1,
flexDirection: 'column'
},
authorContainer: {
flexDirection: 'row',
alignItems: 'center'
},
author: {
fontWeight: 'bold',
marginHorizontal: 5,
flex: 1
},
time: {
fontSize: 10,
fontWeight: 'normal',
color: '#888',
marginLeft: 5
},
fieldsContainer: {
flex: 1,
flexWrap: 'wrap',
flexDirection: 'row'
},
fieldContainer: {
flexDirection: 'column',
padding: 10
},
fieldTitle: {
fontWeight: 'bold'
}
});
const onPress = (attachment) => {
const url = attachment.title_link || attachment.author_link;
if (!url) {
return;
}
openLink(attachment.title_link || attachment.author_link);
};
// Support <http://link|Text>
const formatText = text =>
text.replace(
new RegExp('(?:<|<)((?:https|http):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)', 'gm'),
(match, url, title) => `[${ title }](${ url })`
);
const Reply = ({ attachment, timeFormat }) => {
if (!attachment) {
return null;
}
const renderAvatar = () => {
if (!attachment.author_icon && !attachment.author_name) {
return null;
}
return (
<Avatar
text={attachment.author_name}
size={16}
avatar={attachment.author_icon}
/>
);
};
const renderAuthor = () => (
attachment.author_name ? <Text style={styles.author}>{attachment.author_name}</Text> : null
);
const renderTime = () => {
const time = attachment.ts ? moment(attachment.ts).format(timeFormat) : null;
return time ? <Text style={styles.time}>{ time }</Text> : null;
};
const renderTitle = () => {
if (!(attachment.author_icon || attachment.author_name || attachment.ts)) {
return null;
}
return (
<View style={styles.authorContainer}>
{renderAvatar()}
{renderAuthor()}
{renderTime()}
</View>
);
};
const renderText = () => (
attachment.text ? <Markdown msg={formatText(attachment.text)} /> : null
);
const renderFields = () => {
if (!attachment.fields) {
return null;
}
return (
<View style={styles.fieldsContainer}>
{attachment.fields.map(field => (
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
<Text style={styles.fieldTitle}>{field.title}</Text>
<Text>{field.value}</Text>
</View>
))}
</View>
);
};
return (
<TouchableOpacity
onPress={() => onPress(attachment)}
style={styles.button}
>
<QuoteMark color={attachment.color} />
<View style={styles.attachmentContainer}>
{renderTitle()}
{renderText()}
{renderFields()}
{attachment.attachments && attachment.attachments.map(attach => <Reply key={attach.text} attachment={attach} timeFormat={timeFormat} />)}
</View>
</TouchableOpacity>
);
};
Reply.propTypes = {
attachment: PropTypes.object.isRequired,
timeFormat: PropTypes.string.isRequired
};
export default Reply;

View File

@ -0,0 +1,74 @@
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Image } from 'react-native';
import PropTypes from 'prop-types';
import QuoteMark from './QuoteMark';
import openLink from '../../utils/openLink';
const styles = StyleSheet.create({
button: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
marginVertical: 2
},
quoteSign: {
borderWidth: 2,
borderRadius: 4,
borderColor: '#a0a0a0',
height: '100%',
marginRight: 5
},
image: {
height: 80,
width: 80,
resizeMode: 'cover',
borderRadius: 6
},
textContainer: {
flex: 1,
height: '100%',
flexDirection: 'column',
padding: 4,
justifyContent: 'flex-start',
alignItems: 'flex-start'
},
title: {
fontWeight: 'bold',
fontSize: 12
},
description: {
fontSize: 12
}
});
const onPress = (url) => {
openLink(url);
};
const Url = ({ url }) => {
if (!url) {
return null;
}
return (
<TouchableOpacity onPress={() => onPress(url.url)} style={styles.button}>
<QuoteMark />
{url.image ?
<Image
style={styles.image}
source={{ uri: encodeURI(url.image) }}
/>
: null
}
<View style={styles.textContainer}>
<Text style={styles.title}>{url.title}</Text>
<Text style={styles.description} numberOfLines={1}>{url.description}</Text>
</View>
</TouchableOpacity>
);
};
Url.propTypes = {
url: PropTypes.object.isRequired
};
export default Url;

View File

@ -0,0 +1,83 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, Text, StyleSheet } from 'react-native';
import moment from 'moment';
import Icon from 'react-native-vector-icons/FontAwesome';
import Avatar from '../Avatar';
const styles = StyleSheet.create({
username: {
fontWeight: 'bold'
},
usernameView: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 2
},
alias: {
fontSize: 10,
color: '#888',
paddingLeft: 5
},
time: {
fontSize: 10,
color: '#888',
paddingLeft: 5
},
edited: {
marginLeft: 5,
flexDirection: 'row',
alignItems: 'center'
}
});
export default class User extends React.PureComponent {
static propTypes = {
item: PropTypes.object.isRequired,
Message_TimeFormat: PropTypes.string.isRequired,
onPress: PropTypes.func,
baseUrl: PropTypes.string
}
renderEdited(item) {
if (!item.editedBy) {
return null;
}
return (
<View style={styles.edited}>
<Icon name='pencil-square-o' color='#888' size={10} />
<Avatar
style={{ marginLeft: 5 }}
text={item.editedBy.username}
size={20}
baseUrl={this.props.baseUrl}
avatar={item.avatar}
/>
</View>
);
}
render() {
const { item } = this.props;
const extraStyle = {};
if (item.temp) {
extraStyle.opacity = 0.3;
}
const username = item.alias || item.u.username;
const aliasUsername = item.alias ? (<Text style={styles.alias}>@{item.u.username}</Text>) : null;
const time = moment(item.ts).format(this.props.Message_TimeFormat);
return (
<View style={styles.usernameView}>
<Text onPress={this.props.onPress} style={styles.username}>
{username}
</Text>
{aliasUsername}
<Text style={styles.time}>{time}</Text>
{this.renderEdited(item)}
</View>
);
}
}

View File

@ -0,0 +1,84 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View, StyleSheet, TouchableOpacity, Image, Platform } from 'react-native';
import Modal from 'react-native-modal';
import VideoPlayer from 'react-native-video-controls';
import Markdown from './Markdown';
import openLink from '../../utils/openLink';
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(Platform.OS === 'ios' ? [] : ['video/webm', 'video/3gp', 'video/mkv'])];
const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1;
const styles = StyleSheet.create({
container: {
flex: 1,
height: 100,
margin: 5
},
modal: {
margin: 0,
backgroundColor: '#000'
},
image: {
flex: 1,
width: null,
height: null,
resizeMode: 'contain'
}
});
export default class Video extends React.PureComponent {
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired
}
state = { isVisible: false };
toggleModal() {
this.setState({
isVisible: !this.state.isVisible
});
}
open() {
if (isTypeSupported(this.props.file.video_type)) {
return this.toggleModal();
}
openLink(this.state.uri);
}
render() {
const { isVisible } = this.state;
const { video_url, description } = this.props.file;
const { baseUrl, user } = this.props;
const uri = `${ baseUrl }${ video_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
return (
<View>
<TouchableOpacity
style={styles.container}
onPress={() => this.open()}
>
<Image
source={require('../../../static/images/logo.png')}
style={styles.image}
/>
<Markdown msg={description} />
</TouchableOpacity>
<Modal
isVisible={isVisible}
style={styles.modal}
supportedOrientations={['portrait', 'landscape']}
onBackButtonPress={() => this.toggleModal()}
>
<VideoPlayer
source={{ uri }}
onBack={() => this.toggleModal()}
disableVolume
/>
</Modal>
</View>
);
}
}

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