7
.babelrc
|
@ -1,4 +1,9 @@
|
|||
{
|
||||
"presets": ["react-native"],
|
||||
"plugins": ["transform-decorators-legacy"]
|
||||
"plugins": ["transform-decorators-legacy"],
|
||||
"env": {
|
||||
"production": {
|
||||
"plugins": ["transform-remove-console"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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.
|
|
@ -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 -->
|
|
@ -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/
|
||||
|
|
|
@ -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'
|
|
@ -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.
|
||||
|
66
README.md
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 134 B |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 100 B |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 134 B |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 167 B |
After Width: | Height: | Size: 84 KiB |
After Width: | Height: | Size: 207 B |
|
@ -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>
|
After Width: | Height: | Size: 2.9 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="primary_dark">#660B0B0B</color> </resources>
|
|
@ -1,3 +1,5 @@
|
|||
<resources>
|
||||
<string name="app_name">RocketChatRN</string>
|
||||
|
||||
<string name="no_browser_found">No Browser Found</string>
|
||||
</resources>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import * as types from './actionsTypes';
|
||||
|
||||
export function setActiveUser(data) {
|
||||
return {
|
||||
type: types.ACTIVE_USERS.SET,
|
||||
data
|
||||
};
|
||||
}
|
|
@ -25,3 +25,8 @@ export function disconnect(err) {
|
|||
err
|
||||
};
|
||||
}
|
||||
export function disconnect_by_user() {
|
||||
return {
|
||||
type: types.METEOR.DISCONNECT_BY_USER
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import * as types from './actionsTypes';
|
||||
|
||||
export function setRoles(data) {
|
||||
return {
|
||||
type: types.ROLES.SET,
|
||||
data
|
||||
};
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -41,3 +41,9 @@ export function changedServer(server) {
|
|||
server
|
||||
};
|
||||
}
|
||||
|
||||
export function gotoAddServer() {
|
||||
return {
|
||||
type: SERVER.GOTO_ADD
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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>;
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
};
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
SENT: 0,
|
||||
TEMP: 1,
|
||||
ERROR: 2
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 }` }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 };
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -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>);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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} />);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
import { createIconSetFromIcoMoon } from 'react-native-vector-icons';
|
||||
import iconConfig from '../icons.json';
|
||||
|
||||
export default createIconSetFromIcoMoon(iconConfig);
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|