Merge branch 'develop' into single-server

# Conflicts:
#	android/app/build.gradle
#	app/index.js
#	app/sagas/init.js
#	app/sagas/login.js
#	app/views/RoomsListView/Header/Header.ios.js
#	ios/RocketChatRN.xcodeproj/project.pbxproj
This commit is contained in:
Diego Mello 2020-07-06 15:28:12 -03:00
commit b456e779a0
497 changed files with 30867 additions and 28334 deletions

View File

@ -53,13 +53,30 @@ save-gems-cache: &save-gems-cache
paths: paths:
- vendor/bundle - vendor/bundle
update-fastlane: &update-fastlane update-fastlane-ios: &update-fastlane-ios
name: Update Fastlane name: Update Fastlane
command: | command: |
echo "ruby-2.6.4" > ~/.ruby-version echo "ruby-2.6.4" > ~/.ruby-version
bundle install bundle install
working_directory: ios working_directory: ios
update-fastlane-android: &update-fastlane-android
name: Update Fastlane
command: |
echo "ruby-2.6.4" > ~/.ruby-version
bundle install
working_directory: android
save-gradle-cache: &save-gradle-cache
name: Save gradle cache
key: android-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
paths:
- ~/.gradle
restore_cache: &restore-gradle-cache
name: Restore gradle cache
key: android-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
restore-brew-cache: &restore-brew-cache restore-brew-cache: &restore-brew-cache
name: Restore Brew cache name: Restore Brew cache
key: brew-{{ checksum "yarn.lock" }}-{{ checksum ".circleci/config.yml" }} key: brew-{{ checksum "yarn.lock" }}-{{ checksum ".circleci/config.yml" }}
@ -227,9 +244,9 @@ jobs:
- run: *install-npm-modules - run: *install-npm-modules
- restore_cache: - run: *update-fastlane-android
name: Restore gradle cache
key: android-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }} - restore_cache: *restore-gradle-cache
- run: - run:
name: Configure Gradle name: Configure Gradle
@ -267,14 +284,10 @@ jobs:
name: Build Android App name: Build Android App
command: | command: |
if [[ $KEYSTORE ]]; then if [[ $KEYSTORE ]]; then
./gradlew bundleRelease bundle exec fastlane android release
else else
./gradlew assembleDebug bundle exec fastlane android build
fi fi
mkdir -p /tmp/build
mv app/build/outputs /tmp/build/
working_directory: android working_directory: android
- run: - run:
@ -291,15 +304,40 @@ jobs:
fi fi
- store_artifacts: - store_artifacts:
path: /tmp/build/outputs path: android/app/build/outputs
- save_cache: *save-npm-cache-linux - save_cache: *save-npm-cache-linux
- save_cache: - save_cache: *save-gradle-cache
name: Save gradle cache
key: android-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }} - persist_to_workspace:
root: .
paths: paths:
- ~/.gradle - android/fastlane/report.xml
- android/app/build/outputs
android-google-play-alpha:
<<: *defaults
docker:
- image: circleci/android:api-28-node
steps:
- checkout
- attach_workspace:
at: android
- run:
name: Store the google service account key
command: echo "$FASTLANE_GOOGLE_SERVICE_ACCOUNT" | base64 --decode > service_account.json
working_directory: android
- run: *update-fastlane-android
- run:
name: Fastlane Play Store Upload
command: bundle exec fastlane android alpha
working_directory: android
# iOS builds # iOS builds
ios-build: ios-build:
@ -316,7 +354,7 @@ jobs:
- run: *install-npm-modules - run: *install-npm-modules
- run: *update-fastlane - run: *update-fastlane-ios
- run: - run:
name: Set Google Services name: Set Google Services
@ -378,7 +416,7 @@ jobs:
- restore_cache: *restore-gems-cache - restore_cache: *restore-gems-cache
- run: *update-fastlane - run: *update-fastlane-ios
- run: - run:
name: Fastlane Tesflight Upload name: Fastlane Tesflight Upload
@ -424,3 +462,10 @@ workflows:
- android-build: - android-build:
requires: requires:
- lint-testunit - lint-testunit
- android-hold-google-play-alpha:
type: approval
requires:
- android-build
- android-google-play-alpha:
requires:
- android-hold-google-play-alpha

7
.gitignore vendored
View File

@ -51,9 +51,10 @@ buck-out/
# For more information about the recommended setup visit: # For more information about the recommended setup visit:
# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
fastlane/report.xml **/fastlane/report.xml
fastlane/Preview.html **/fastlane/Preview.html
fastlane/screenshots **/fastlane/screenshots
**/fastlane/test_output
coverage coverage

View File

@ -1 +1,3 @@
{} {
"content_hash_max_items": 360000
}

View File

@ -69,7 +69,7 @@ Follow the [React Native Getting Started Guide](https://facebook.github.io/react
### Running single server ### Running single server
If you don't need multiple servers, there is a branch `single-server` just for that. If you don't need multiple servers, there is a branch `single-server` just for that.
Readme will guide you on how to config. Temp whitelabel docs: https://docs.google.com/document/d/17ib2Le_SH6U2gP0sEuKapF2J-WgqZxPFMRRVUSofz7Y/edit
## Current priorities ## Current priorities
1) Omnichannel support 1) Omnichannel support

6
android/Gemfile Normal file
View File

@ -0,0 +1,6 @@
source "https://rubygems.org"
gem "fastlane"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

181
android/Gemfile.lock Normal file
View File

@ -0,0 +1,181 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.2)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
aws-eventstream (1.0.3)
aws-partitions (1.294.0)
aws-sdk-core (3.92.0)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.30.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.61.2)
aws-sdk-core (~> 3, >= 3.83.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.1)
aws-eventstream (~> 1.0, >= 1.0.2)
babosa (1.0.3)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
declarative (0.0.10)
declarative-option (0.1.0)
digest-crc (0.5.1)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.5)
emoji_regex (1.0.1)
excon (0.73.0)
faraday (0.17.3)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.6)
faraday (>= 0.7.4)
http-cookie (~> 1.0.0)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
fastimage (2.1.7)
fastlane (2.145.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.2, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 0.17)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 0.13.1)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.29.2, < 0.37.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
json (< 3.0.0)
jwt (~> 2.1.0)
mini_magick (>= 4.9.4, < 5.0.0)
multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
plist (>= 3.1.0, < 4.0.0)
public_suffix (~> 2.0.0)
rubyzip (>= 1.3.0, < 2.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-appcenter (1.8.0)
gh_inspector (1.1.3)
google-api-client (0.36.4)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.12)
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.3.1)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.0)
google-cloud-storage (1.25.1)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
googleauth (0.11.0)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.12)
highline (1.7.10)
http-cookie (1.0.3)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.4.0)
json (2.3.0)
jwt (2.1.0)
memoist (0.16.2)
mini_magick (4.10.1)
mini_mime (1.0.2)
multi_json (1.14.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
nanaimo (0.2.6)
naturally (2.2.0)
os (1.1.0)
plist (3.5.0)
public_suffix (2.0.5)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rouge (2.0.7)
rubyzip (1.3.0)
security (0.1.3)
signet (0.14.0)
addressable (~> 2.3)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
CFPropertyList
naturally
slack-notifier (2.3.2)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
tty-cursor (0.7.1)
tty-screen (0.7.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7-x64-mingw32)
unicode-display_width (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.15.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.2.6)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.0)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
x64-mingw32
DEPENDENCIES
fastlane
fastlane-plugin-appcenter
BUNDLED WITH
2.0.2

View File

@ -3,13 +3,8 @@
package="chat.rocket.reactnative"> package="chat.rocket.reactnative">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<!-- <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> -->
<!-- <uses-permission-sdk-23 android:name="android.permission.VIBRATE"/> -->
<application <application
android:name=".MainApplication" android:name=".MainApplication"
@ -65,6 +60,7 @@
android:theme="@style/AppTheme" > android:theme="@style/AppTheme" >
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" /> <data android:mimeType="*/*" />
</intent-filter> </intent-filter>

BIN
android/app/src/main/assets/fonts/custom.ttf Executable file → Normal file

Binary file not shown.

View File

@ -13,7 +13,8 @@ public class MainActivity extends ReactFragmentActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // https://github.com/software-mansion/react-native-screens/issues/17#issuecomment-424704067
super.onCreate(null);
RNBootSplash.init(R.drawable.launch_screen, MainActivity.this); RNBootSplash.init(R.drawable.launch_screen, MainActivity.this);
} }

View File

@ -15,6 +15,7 @@ public class BasePackageList {
new expo.modules.keepawake.KeepAwakePackage(), new expo.modules.keepawake.KeepAwakePackage(),
new expo.modules.localauthentication.LocalAuthenticationPackage(), new expo.modules.localauthentication.LocalAuthenticationPackage(),
new expo.modules.permissions.PermissionsPackage(), new expo.modules.permissions.PermissionsPackage(),
new expo.modules.videothumbnails.VideoThumbnailsPackage(),
new expo.modules.webbrowser.WebBrowserPackage() new expo.modules.webbrowser.WebBrowserPackage()
); );
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 942 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 949 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

2
android/fastlane/Appfile Normal file
View File

@ -0,0 +1,2 @@
json_key_file("service_account.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
package_name("chat.rocket.reactnative")

36
android/fastlane/Fastfile Normal file
View File

@ -0,0 +1,36 @@
# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
# https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
# https://docs.fastlane.tools/plugins/available-plugins
#
# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
default_platform(:android)
platform :android do
desc "Build App for development"
lane :build do
gradle(task: "assembleDebug")
end
desc "Build App for release"
lane :release do
gradle(task: "bundleRelease")
end
desc "Upload App to Play store"
lane :alpha do
upload_to_play_store(
track: 'alpha',
aab: 'android/app/build/outputs/bundle/release/app-release.aab'
)
end
end

View File

@ -0,0 +1,5 @@
# Autogenerated by fastlane
#
# Ensure this file is checked in to source control!

View File

@ -0,0 +1,39 @@
fastlane documentation
================
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```
xcode-select --install
```
Install _fastlane_ using
```
[sudo] gem install fastlane -NV
```
or alternatively using `brew cask install fastlane`
# Available Actions
## Android
### android build
```
fastlane android build
```
Build App for development
### android release
```
fastlane android release
```
Build App for release
### android alpha
```
fastlane android alpha
```
Upload App to Play store
----
This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run.
More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).

View File

@ -26,7 +26,7 @@ android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX # Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true android.enableJetifier=true
APPLICATIONID=chat.rocket.reactnative APPLICATIONID=chat.rocket.reactnative
VERSIONNAME=4.5.1 VERSIONNAME=4.8.0
VERSIONCODE=1 VERSIONCODE=1
BugsnagAPIKey="" BugsnagAPIKey=""
KEYSTORE=my-upload-key.keystore KEYSTORE=my-upload-key.keystore

114
app/AppContainer.js Normal file
View File

@ -0,0 +1,114 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { connect } from 'react-redux';
import Navigation from './lib/Navigation';
import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation';
import {
ROOT_LOADING, ROOT_OUTSIDE, ROOT_NEW_SERVER, ROOT_INSIDE, ROOT_SET_USERNAME, ROOT_BACKGROUND
} from './actions/app';
// Stacks
import AuthLoadingView from './views/AuthLoadingView';
// SetUsername Stack
import SetUsernameView from './views/SetUsernameView';
import OutsideStack from './stacks/OutsideStack';
import InsideStack from './stacks/InsideStack';
import MasterDetailStack from './stacks/MasterDetailStack';
import { ThemeContext } from './theme';
import { setCurrentScreen } from './utils/log';
// SetUsernameStack
const SetUsername = createStackNavigator();
const SetUsernameStack = () => (
<SetUsername.Navigator screenOptions={defaultHeader}>
<SetUsername.Screen
name='SetUsernameView'
component={SetUsernameView}
/>
</SetUsername.Navigator>
);
// App
const Stack = createStackNavigator();
const App = React.memo(({ root, isMasterDetail }) => {
if (!root) {
return null;
}
const { theme } = React.useContext(ThemeContext);
const navTheme = navigationTheme(theme);
React.useEffect(() => {
const state = Navigation.navigationRef.current?.getRootState();
const currentRouteName = getActiveRouteName(state);
Navigation.routeNameRef.current = currentRouteName;
setCurrentScreen(currentRouteName);
}, []);
return (
<NavigationContainer
theme={navTheme}
ref={Navigation.navigationRef}
onStateChange={(state) => {
const previousRouteName = Navigation.routeNameRef.current;
const currentRouteName = getActiveRouteName(state);
if (previousRouteName !== currentRouteName) {
setCurrentScreen(currentRouteName);
}
Navigation.routeNameRef.current = currentRouteName;
}}
>
<Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}>
<>
{root === ROOT_LOADING || root === ROOT_BACKGROUND ? (
<Stack.Screen
name='AuthLoading'
component={AuthLoadingView}
/>
) : null}
{root === ROOT_OUTSIDE || root === ROOT_NEW_SERVER ? (
<Stack.Screen
name='OutsideStack'
component={OutsideStack}
/>
) : null}
{root === ROOT_INSIDE && isMasterDetail ? (
<Stack.Screen
name='MasterDetailStack'
component={MasterDetailStack}
/>
) : null}
{root === ROOT_INSIDE && !isMasterDetail ? (
<Stack.Screen
name='InsideStack'
component={InsideStack}
/>
) : null}
{root === ROOT_SET_USERNAME ? (
<Stack.Screen
name='SetUsernameStack'
component={SetUsernameStack}
/>
) : null}
</>
</Stack.Navigator>
</NavigationContainer>
);
});
const mapStateToProps = state => ({
root: state.app.root,
isMasterDetail: state.app.isMasterDetail
});
App.propTypes = {
root: PropTypes.string,
isMasterDetail: PropTypes.bool
};
const AppContainer = connect(mapStateToProps)(App);
export default AppContainer;

View File

@ -12,7 +12,8 @@ function createRequestTypes(base, types = defaultTypes) {
export const LOGIN = createRequestTypes('LOGIN', [ export const LOGIN = createRequestTypes('LOGIN', [
...defaultTypes, ...defaultTypes,
'SET_SERVICES', 'SET_SERVICES',
'SET_PREFERENCE' 'SET_PREFERENCE',
'SET_LOCAL_AUTHENTICATED'
]); ]);
export const SHARE = createRequestTypes('SHARE', [ export const SHARE = createRequestTypes('SHARE', [
'SELECT_SERVER', 'SELECT_SERVER',
@ -32,7 +33,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
'CLOSE_SEARCH_HEADER' 'CLOSE_SEARCH_HEADER'
]); ]);
export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'CLOSE', 'FORWARD', 'USER_TYPING']); export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'CLOSE', 'FORWARD', 'USER_TYPING']);
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS']); export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS', 'SET_MASTER_DETAIL']);
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']); export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]); export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
export const CREATE_DISCUSSION = createRequestTypes('CREATE_DISCUSSION', [...defaultTypes]); export const CREATE_DISCUSSION = createRequestTypes('CREATE_DISCUSSION', [...defaultTypes]);
@ -50,7 +51,6 @@ export const LOGOUT = 'LOGOUT'; // logout is always success
export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']); export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']); export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']); export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);
export const NOTIFICATION = createRequestTypes('NOTIFICATION', ['RECEIVED', 'REMOVE']);
export const TOGGLE_CRASH_REPORT = 'TOGGLE_CRASH_REPORT'; export const TOGGLE_CRASH_REPORT = 'TOGGLE_CRASH_REPORT';
export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS'; export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
export const SET_ACTIVE_USERS = 'SET_ACTIVE_USERS'; export const SET_ACTIVE_USERS = 'SET_ACTIVE_USERS';

41
app/actions/app.js Normal file
View File

@ -0,0 +1,41 @@
import { APP } from './actionsTypes';
export const ROOT_OUTSIDE = 'outside';
export const ROOT_INSIDE = 'inside';
export const ROOT_LOADING = 'loading';
export const ROOT_NEW_SERVER = 'newServer';
export const ROOT_SET_USERNAME = 'setUsername';
export const ROOT_BACKGROUND = 'background';
export function appStart({ root, ...args }) {
return {
type: APP.START,
root,
...args
};
}
export function appReady() {
return {
type: APP.READY
};
}
export function appInit() {
return {
type: APP.INIT
};
}
export function appInitLocalSettings() {
return {
type: APP.INIT_LOCAL_SETTINGS
};
}
export function setMasterDetail(isMasterDetail) {
return {
type: APP.SET_MASTER_DETAIL,
isMasterDetail
};
}

View File

@ -1,41 +0,0 @@
import * as types from '../constants/types';
import { APP } from './actionsTypes';
export function appStart(root, text) {
return {
type: APP.START,
root,
text
};
}
export function appReady() {
return {
type: APP.READY
};
}
export function appInit() {
return {
type: APP.INIT
};
}
export function appInitLocalSettings() {
return {
type: APP.INIT_LOCAL_SETTINGS
};
}
export function setCurrentServer(server) {
return {
type: types.SET_CURRENT_SERVER,
payload: server
};
}
export function login() {
return {
type: 'LOGIN'
};
}

View File

@ -49,3 +49,10 @@ export function setPreference(preference) {
preference preference
}; };
} }
export function setLocalAuthenticated(isLocalAuthenticated) {
return {
type: types.LOGIN.SET_LOCAL_AUTHENTICATED,
isLocalAuthenticated
};
}

View File

@ -1,19 +0,0 @@
import { NOTIFICATION } from './actionsTypes';
export function notificationReceived(params) {
return {
type: NOTIFICATION.RECEIVED,
payload: {
title: params.title,
avatar: params.avatar,
message: params.text,
payload: params.payload
}
};
}
export function removeNotification() {
return {
type: NOTIFICATION.REMOVE
};
}

View File

@ -44,9 +44,10 @@ export function serverFailure(err) {
}; };
} }
export function serverInitAdd() { export function serverInitAdd(previousServer) {
return { return {
type: SERVER.INIT_ADD type: SERVER.INIT_ADD,
previousServer
}; };
} }

View File

@ -1,5 +1,5 @@
/* eslint-disable no-bitwise */ /* eslint-disable no-bitwise */
import { constants } from 'react-native-keycommands'; import KeyCommands, { constants } from 'react-native-keycommands';
import I18n from './i18n'; import I18n from './i18n';
@ -17,7 +17,7 @@ const KEY_ADD_SERVER = __DEV__ ? 'l' : 'n';
const KEY_SEND_MESSAGE = '\r'; const KEY_SEND_MESSAGE = '\r';
const KEY_SELECT = '123456789'; const KEY_SELECT = '123456789';
export const defaultCommands = [ const keyCommands = [
{ {
// Focus messageBox // Focus messageBox
input: KEY_TYPING, input: KEY_TYPING,
@ -29,10 +29,7 @@ export const defaultCommands = [
input: KEY_SEND_MESSAGE, input: KEY_SEND_MESSAGE,
modifierFlags: 0, modifierFlags: 0,
discoverabilityTitle: I18n.t('Send') discoverabilityTitle: I18n.t('Send')
} },
];
export const keyCommands = [
{ {
// Open Preferences Modal // Open Preferences Modal
input: KEY_PREFERENCES, input: KEY_PREFERENCES,
@ -139,6 +136,10 @@ export const keyCommands = [
}))) })))
]; ];
export const setKeyCommands = () => KeyCommands.setKeyCommands(keyCommands);
export const deleteKeyCommands = () => KeyCommands.deleteKeyCommands(keyCommands);
export const KEY_COMMAND = 'KEY_COMMAND'; export const KEY_COMMAND = 'KEY_COMMAND';
export const commandHandle = (event, key, flags = []) => { export const commandHandle = (event, key, flags = []) => {

View File

@ -53,7 +53,9 @@ export const themes = {
passcodePrimary: '#2F343D', passcodePrimary: '#2F343D',
passcodeSecondary: '#6C727A', passcodeSecondary: '#6C727A',
passcodeDotEmpty: '#CBCED1', passcodeDotEmpty: '#CBCED1',
passcodeDotFull: '#6C727A' passcodeDotFull: '#6C727A',
previewBackground: '#1F2329',
previewTintColor: '#ffffff'
}, },
dark: { dark: {
backgroundColor: '#030b1b', backgroundColor: '#030b1b',
@ -95,7 +97,9 @@ export const themes = {
passcodePrimary: '#FFFFFF', passcodePrimary: '#FFFFFF',
passcodeSecondary: '#CBCED1', passcodeSecondary: '#CBCED1',
passcodeDotEmpty: '#CBCED1', passcodeDotEmpty: '#CBCED1',
passcodeDotFull: '#6C727A' passcodeDotFull: '#6C727A',
previewBackground: '#030b1b',
previewTintColor: '#ffffff'
}, },
black: { black: {
backgroundColor: '#000000', backgroundColor: '#000000',
@ -137,6 +141,8 @@ export const themes = {
passcodePrimary: '#FFFFFF', passcodePrimary: '#FFFFFF',
passcodeSecondary: '#CBCED1', passcodeSecondary: '#CBCED1',
passcodeDotEmpty: '#CBCED1', passcodeDotEmpty: '#CBCED1',
passcodeDotFull: '#6C727A' passcodeDotFull: '#6C727A',
previewBackground: '#000000',
previewTintColor: '#ffffff'
} }
}; };

View File

@ -50,6 +50,18 @@ export default {
Accounts_ManuallyApproveNewUsers: { Accounts_ManuallyApproveNewUsers: {
type: 'valueAsBoolean' type: 'valueAsBoolean'
}, },
API_Use_REST_For_DDP_Calls: {
type: 'valueAsBoolean'
},
Accounts_iframe_enabled: {
type: 'valueAsBoolean'
},
Accounts_Iframe_api_url: {
type: 'valueAsString'
},
Accounts_Iframe_api_method: {
type: 'valueAsString'
},
CROWD_Enable: { CROWD_Enable: {
type: 'valueAsBoolean' type: 'valueAsBoolean'
}, },

View File

@ -1,4 +1,3 @@
export const MAX_SIDEBAR_WIDTH = 321; export const MAX_SIDEBAR_WIDTH = 321;
export const MAX_CONTENT_WIDTH = '90%';
export const MAX_SCREEN_CONTENT_WIDTH = '50%'; export const MAX_SCREEN_CONTENT_WIDTH = '50%';
export const MIN_WIDTH_SPLIT_LAYOUT = 700; export const MIN_WIDTH_MASTER_DETAIL_LAYOUT = 700;

View File

@ -1,4 +0,0 @@
export const SET_CURRENT_SERVER = 'SET_CURRENT_SERVER';
export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
export const ADD_SETTINGS = 'ADD_SETTINGS';
export const CLEAR_SETTINGS = 'CLEAR_SETTINGS';

View File

@ -0,0 +1,210 @@
import React, {
useRef,
useState,
useEffect,
forwardRef,
useImperativeHandle,
useCallback,
isValidElement
} from 'react';
import PropTypes from 'prop-types';
import { Keyboard, Text } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { TapGestureHandler, State } from 'react-native-gesture-handler';
import ScrollBottomSheet from 'react-native-scroll-bottom-sheet';
import Animated, {
Extrapolate,
interpolate,
Value,
Easing
} from 'react-native-reanimated';
import * as Haptics from 'expo-haptics';
import { useBackHandler } from '@react-native-community/hooks';
import { Item } from './Item';
import { Handle } from './Handle';
import { Button } from './Button';
import { themes } from '../../constants/colors';
import styles, { ITEM_HEIGHT } from './styles';
import { isTablet, isIOS } from '../../utils/deviceInfo';
import Separator from '../Separator';
import I18n from '../../i18n';
import { useOrientation, useDimensions } from '../../dimensions';
const getItemLayout = (data, index) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index });
const HANDLE_HEIGHT = isIOS ? 40 : 56;
const MAX_SNAP_HEIGHT = 16;
const CANCEL_HEIGHT = 64;
const ANIMATION_DURATION = 250;
const ANIMATION_CONFIG = {
duration: ANIMATION_DURATION,
// https://easings.net/#easeInOutCubic
easing: Easing.bezier(0.645, 0.045, 0.355, 1.0)
};
const ActionSheet = React.memo(forwardRef(({ children, theme }, ref) => {
const bottomSheetRef = useRef();
const [data, setData] = useState({});
const [isVisible, setVisible] = useState(false);
const { height } = useDimensions();
const { isLandscape } = useOrientation();
const insets = useSafeAreaInsets();
const maxSnap = Math.max(
(
height
// Items height
- (ITEM_HEIGHT * (data?.options?.length || 0))
// Handle height
- HANDLE_HEIGHT
// Custom header height
- (data?.headerHeight || 0)
// Insets bottom height (Notch devices)
- insets.bottom
// Cancel button height
- (data?.hasCancel ? CANCEL_HEIGHT : 0)
),
MAX_SNAP_HEIGHT
);
/*
* if the action sheet cover more
* than 60% of the whole screen
* and it's not at the landscape mode
* we'll provide more one snap
* that point 50% of the whole screen
*/
const snaps = (height - maxSnap > height * 0.6) && !isLandscape ? [maxSnap, height * 0.5, height] : [maxSnap, height];
const openedSnapIndex = snaps.length > 2 ? 1 : 0;
const closedSnapIndex = snaps.length - 1;
const toggleVisible = () => setVisible(!isVisible);
const hide = () => {
bottomSheetRef.current?.snapTo(closedSnapIndex);
};
const show = (options) => {
setData(options);
toggleVisible();
};
const onBackdropPressed = ({ nativeEvent }) => {
if (nativeEvent.oldState === State.ACTIVE) {
hide();
}
};
useBackHandler(() => {
if (isVisible) {
hide();
}
return isVisible;
});
useEffect(() => {
if (isVisible) {
Keyboard.dismiss();
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
bottomSheetRef.current?.snapTo(openedSnapIndex);
}
}, [isVisible]);
// Hides action sheet when orientation changes
useEffect(() => {
setVisible(false);
}, [isLandscape]);
useImperativeHandle(ref, () => ({
showActionSheet: show,
hideActionSheet: hide
}));
const renderHandle = useCallback(() => (
<>
<Handle theme={theme} />
{isValidElement(data?.customHeader) ? data.customHeader : null}
</>
));
const renderFooter = useCallback(() => (data?.hasCancel ? (
<Button
onPress={hide}
style={[styles.button, { backgroundColor: themes[theme].auxiliaryBackground }]}
theme={theme}
>
<Text style={[styles.text, { color: themes[theme].bodyText }]}>
{I18n.t('Cancel')}
</Text>
</Button>
) : null));
const renderSeparator = useCallback(() => <Separator theme={theme} style={styles.separator} />);
const renderItem = useCallback(({ item }) => <Item item={item} hide={hide} theme={theme} />);
const animatedPosition = React.useRef(new Value(0));
const opacity = interpolate(animatedPosition.current, {
inputRange: [0, 1],
outputRange: [0, 0.7],
extrapolate: Extrapolate.CLAMP
});
return (
<>
{children}
{isVisible && (
<>
<TapGestureHandler onHandlerStateChange={onBackdropPressed}>
<Animated.View
testID='action-sheet-backdrop'
style={[
styles.backdrop,
{
backgroundColor: themes[theme].backdropColor,
opacity
}
]}
/>
</TapGestureHandler>
<ScrollBottomSheet
testID='action-sheet'
ref={bottomSheetRef}
componentType='FlatList'
snapPoints={snaps}
initialSnapIndex={closedSnapIndex}
renderHandle={renderHandle}
onSettle={index => (index === closedSnapIndex) && toggleVisible()}
animatedPosition={animatedPosition.current}
containerStyle={[
styles.container,
{ backgroundColor: themes[theme].focusedBackground },
(isLandscape || isTablet) && styles.bottomSheet
]}
animationConfig={ANIMATION_CONFIG}
// FlatList props
data={data?.options}
renderItem={renderItem}
keyExtractor={item => item.title}
style={{ backgroundColor: themes[theme].focusedBackground }}
contentContainerStyle={styles.content}
ItemSeparatorComponent={renderSeparator}
ListHeaderComponent={renderSeparator}
ListFooterComponent={renderFooter}
getItemLayout={getItemLayout}
removeClippedSubviews={isIOS}
/>
</>
)}
</>
);
}));
ActionSheet.propTypes = {
children: PropTypes.node,
theme: PropTypes.string
};
export default ActionSheet;

View File

@ -0,0 +1,7 @@
import { TouchableOpacity } from 'react-native';
import { isAndroid } from '../../utils/deviceInfo';
import Touch from '../../utils/touch';
// Taken from https://github.com/rgommezz/react-native-scroll-bottom-sheet#touchables
export const Button = isAndroid ? Touch : TouchableOpacity;

View File

@ -0,0 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import styles from './styles';
import { themes } from '../../constants/colors';
export const Handle = React.memo(({ theme }) => (
<View style={[styles.handle, { backgroundColor: themes[theme].focusedBackground }]} testID='action-sheet-handle'>
<View style={[styles.handleIndicator, { backgroundColor: themes[theme].auxiliaryText }]} />
</View>
));
Handle.propTypes = {
theme: PropTypes.string
};

View File

@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text } from 'react-native';
import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons';
import styles from './styles';
import { Button } from './Button';
export const Item = React.memo(({ item, hide, theme }) => {
const onPress = () => {
hide();
item?.onPress();
};
return (
<Button
onPress={onPress}
style={[styles.item, { backgroundColor: themes[theme].focusedBackground }]}
theme={theme}
>
<CustomIcon name={item.icon} size={20} color={item.danger ? themes[theme].dangerColor : themes[theme].bodyText} />
<Text
numberOfLines={1}
style={[styles.title, { color: item.danger ? themes[theme].dangerColor : themes[theme].bodyText }]}
>
{item.title}
</Text>
</Button>
);
});
Item.propTypes = {
item: PropTypes.shape({
title: PropTypes.string,
icon: PropTypes.string,
danger: PropTypes.bool,
onPress: PropTypes.func
}),
hide: PropTypes.func,
theme: PropTypes.string
};

View File

@ -0,0 +1,45 @@
import React, { useRef, useContext, forwardRef } from 'react';
import PropTypes from 'prop-types';
import ActionSheet from './ActionSheet';
import { useTheme } from '../../theme';
const context = React.createContext({
showActionSheet: () => {},
hideActionSheet: () => {}
});
export const useActionSheet = () => useContext(context);
const { Provider, Consumer } = context;
export const withActionSheet = Component => forwardRef((props, ref) => (
<Consumer>
{contexts => <Component {...props} {...contexts} ref={ref} />}
</Consumer>
));
export const ActionSheetProvider = React.memo(({ children }) => {
const ref = useRef();
const { theme } = useTheme();
const getContext = () => ({
showActionSheet: (options) => {
ref.current?.showActionSheet(options);
},
hideActionSheet: () => {
ref.current?.hideActionSheet();
}
});
return (
<Provider value={getContext()}>
<ActionSheet ref={ref} theme={theme}>
{children}
</ActionSheet>
</Provider>
);
});
ActionSheetProvider.propTypes = {
children: PropTypes.node
};

View File

@ -0,0 +1,2 @@
export * from './Provider';
export * from './Button';

View File

@ -0,0 +1,61 @@
import { StyleSheet } from 'react-native';
import sharedStyles from '../../views/Styles';
export const ITEM_HEIGHT = 48;
export default StyleSheet.create({
container: {
overflow: 'hidden',
borderTopLeftRadius: 16,
borderTopRightRadius: 16
},
item: {
paddingHorizontal: 16,
height: ITEM_HEIGHT,
alignItems: 'center',
flexDirection: 'row'
},
separator: {
marginHorizontal: 16
},
content: {
paddingTop: 16
},
title: {
fontSize: 16,
marginLeft: 16,
...sharedStyles.textRegular
},
handle: {
justifyContent: 'center',
alignItems: 'center'
},
handleIndicator: {
width: 40,
height: 4,
borderRadius: 4,
margin: 8
},
backdrop: {
...StyleSheet.absoluteFillObject
},
bottomSheet: {
width: '50%',
alignSelf: 'center',
left: '25%'
},
button: {
marginHorizontal: 16,
paddingHorizontal: 14,
justifyContent: 'center',
height: ITEM_HEIGHT,
borderRadius: 2,
marginBottom: 12
},
text: {
fontSize: 16,
textAlign: 'center',
...sharedStyles.textMedium
}
});

View File

@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import { View, Image, StyleSheet } from 'react-native'; import { View, StyleSheet } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { CustomIcon } from '../lib/Icons';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
disclosureContainer: { disclosureContainer: {
@ -10,17 +11,14 @@ const styles = StyleSheet.create({
marginRight: 9, marginRight: 9,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center' justifyContent: 'center'
},
disclosureIndicator: {
width: 20,
height: 20
} }
}); });
export const DisclosureImage = React.memo(({ theme }) => ( export const DisclosureImage = React.memo(({ theme }) => (
<Image <CustomIcon
source={{ uri: 'disclosure_indicator' }} name='chevron-right'
style={[styles.disclosureIndicator, { tintColor: themes[theme].auxiliaryTintColor }]} color={themes[theme].auxiliaryTintColor}
size={20}
/> />
)); ));
DisclosureImage.propTypes = { DisclosureImage.propTypes = {

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Text, TouchableOpacity, FlatList } from 'react-native'; import { Text, TouchableOpacity, FlatList } from 'react-native';
import { responsive } from 'react-native-responsive-ui';
import shortnameToUnicode from '../../utils/shortnameToUnicode'; import shortnameToUnicode from '../../utils/shortnameToUnicode';
import styles from './styles'; import styles from './styles';
@ -25,7 +24,6 @@ class EmojiCategory extends React.Component {
static propTypes = { static propTypes = {
baseUrl: PropTypes.string.isRequired, baseUrl: PropTypes.string.isRequired,
emojis: PropTypes.any, emojis: PropTypes.any,
window: PropTypes.any,
onEmojiSelected: PropTypes.func, onEmojiSelected: PropTypes.func,
emojisPerRow: PropTypes.number, emojisPerRow: PropTypes.number,
width: PropTypes.number width: PropTypes.number
@ -73,4 +71,4 @@ class EmojiCategory extends React.Component {
} }
} }
export default responsive(EmojiCategory); export default EmojiCategory;

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { ScrollView, StyleSheet, View } from 'react-native'; import { ScrollView, StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { SafeAreaView } from 'react-navigation';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
@ -10,6 +9,7 @@ import KeyboardView from '../presentation/KeyboardView';
import StatusBar from './StatusBar'; import StatusBar from './StatusBar';
import AppVersion from './AppVersion'; import AppVersion from './AppVersion';
import { isTablet } from '../utils/deviceInfo'; import { isTablet } from '../utils/deviceInfo';
import SafeAreaView from './SafeAreaView';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
scrollView: { scrollView: {
@ -31,7 +31,7 @@ const FormContainer = ({ children, theme, testID }) => (
> >
<StatusBar theme={theme} /> <StatusBar theme={theme} />
<ScrollView {...scrollPersistTaps} style={sharedStyles.container} contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}> <ScrollView {...scrollPersistTaps} style={sharedStyles.container} contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}>
<SafeAreaView style={sharedStyles.container} forceInset={{ top: 'never' }} testID={testID}> <SafeAreaView testID={testID} theme={theme} style={{ backgroundColor: themes[theme].backgroundColor }}>
{children} {children}
<AppVersion theme={theme} /> <AppVersion theme={theme} />
</SafeAreaView> </SafeAreaView>

View File

@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import { SafeAreaView } from 'react-native-safe-area-context';
import { View, StyleSheet } from 'react-native';
import { themes } from '../../constants/colors';
import { themedHeader } from '../../utils/navigation';
import { isIOS, isTablet } from '../../utils/deviceInfo';
// Get from https://github.com/react-navigation/react-navigation/blob/master/packages/stack/src/views/Header/HeaderSegment.tsx#L69
export const headerHeight = isIOS ? 44 : 56;
export const getHeaderHeight = (isLandscape) => {
if (isIOS) {
if (isLandscape && !isTablet) {
return 32;
} else {
return 44;
}
}
return 56;
};
const styles = StyleSheet.create({
container: {
height: headerHeight,
flexDirection: 'row',
justifyContent: 'center',
elevation: 4
}
});
const Header = ({
theme, headerLeft, headerTitle, headerRight
}) => (
<SafeAreaView style={{ backgroundColor: themes[theme].headerBackground }} edges={['top', 'left', 'right']}>
<View style={[styles.container, { ...themedHeader(theme).headerStyle }]}>
{headerLeft ? headerLeft() : null}
{headerTitle ? headerTitle() : null}
{headerRight ? headerRight() : null}
</View>
</SafeAreaView>
);
Header.propTypes = {
theme: PropTypes.string,
headerLeft: PropTypes.element,
headerTitle: PropTypes.element,
headerRight: PropTypes.element
};
export default Header;

View File

@ -36,9 +36,11 @@ export const DrawerButton = React.memo(({ navigation, testID, ...otherProps }) =
</CustomHeaderButtons> </CustomHeaderButtons>
)); ));
export const CloseModalButton = React.memo(({ navigation, testID, onPress = () => navigation.pop() }) => ( export const CloseModalButton = React.memo(({
navigation, testID, onPress = () => navigation.pop(), ...props
}) => (
<CustomHeaderButtons left> <CustomHeaderButtons left>
<Item title='close' iconName='cross' onPress={onPress} testID={testID} /> <Item title='close' iconName='Cross' onPress={onPress} testID={testID} {...props} />
</CustomHeaderButtons> </CustomHeaderButtons>
)); ));
@ -46,7 +48,7 @@ export const CancelModalButton = React.memo(({ onPress, testID }) => (
<CustomHeaderButtons left> <CustomHeaderButtons left>
{isIOS {isIOS
? <Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} /> ? <Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} />
: <Item title='close' iconName='cross' onPress={onPress} testID={testID} /> : <Item title='close' iconName='Cross' onPress={onPress} testID={testID} />
} }
</CustomHeaderButtons> </CustomHeaderButtons>
)); ));
@ -57,9 +59,9 @@ export const MoreButton = React.memo(({ onPress, testID }) => (
</CustomHeaderButtons> </CustomHeaderButtons>
)); ));
export const SaveButton = React.memo(({ onPress, testID }) => ( export const SaveButton = React.memo(({ onPress, testID, ...props }) => (
<CustomHeaderButtons> <CustomHeaderButtons>
<Item title='save' iconName='Download' onPress={onPress} testID={testID} /> <Item title='save' iconName='download' onPress={onPress} testID={testID} {...props} />
</CustomHeaderButtons> </CustomHeaderButtons>
)); ));

View File

@ -0,0 +1,147 @@
import React from 'react';
import { StyleSheet, View, Text } from 'react-native';
import PropTypes from 'prop-types';
import Touchable from 'react-native-platform-touchable';
import { connect } from 'react-redux';
import { Notifier } from 'react-native-notifier';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Avatar from '../Avatar';
import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
import { useTheme } from '../../theme';
import { getUserSelector } from '../../selectors/login';
import { ROW_HEIGHT } from '../../presentation/RoomItem';
import { goRoom } from '../../utils/goRoom';
import Navigation from '../../lib/Navigation';
import { useOrientation } from '../../dimensions';
const AVATAR_SIZE = 48;
const BUTTON_HIT_SLOP = {
top: 12, right: 12, bottom: 12, left: 12
};
const styles = StyleSheet.create({
container: {
height: ROW_HEIGHT,
paddingHorizontal: 14,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginHorizontal: 10,
borderWidth: StyleSheet.hairlineWidth,
borderRadius: 4
},
content: {
flex: 1,
flexDirection: 'row',
alignItems: 'center'
},
inner: {
flex: 1
},
avatar: {
marginRight: 10
},
roomName: {
fontSize: 17,
lineHeight: 20,
...sharedStyles.textMedium
},
message: {
fontSize: 14,
lineHeight: 17,
...sharedStyles.textRegular
},
close: {
marginLeft: 10
},
small: {
width: '50%',
alignSelf: 'center'
}
});
const hideNotification = () => Notifier.hideNotification();
const NotifierComponent = React.memo(({
baseUrl, user, notification, isMasterDetail
}) => {
const { theme } = useTheme();
const insets = useSafeAreaInsets();
const { isLandscape } = useOrientation();
const { id: userId, token } = user;
const { text, payload } = notification;
const { type } = payload;
const name = type === 'd' ? payload.sender.username : payload.name;
// if sub is not on local database, title and avatar will be null, so we use payload from notification
const { title = name, avatar = name } = notification;
const onPress = () => {
const { rid, prid } = payload;
if (!rid) {
return;
}
const item = {
rid, name: title, t: type, prid
};
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
}
goRoom({ item, isMasterDetail });
hideNotification();
};
return (
<View style={[
styles.container,
(isMasterDetail || isLandscape) && styles.small,
{
backgroundColor: themes[theme].focusedBackground,
borderColor: themes[theme].separatorColor,
marginTop: insets.top
}
]}
>
<Touchable
style={styles.content}
onPress={onPress}
hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()}
>
<>
<Avatar text={avatar} size={AVATAR_SIZE} type={type} baseUrl={baseUrl} style={styles.avatar} userId={userId} token={token} />
<View style={styles.inner}>
<Text style={[styles.roomName, { color: themes[theme].titleText }]} numberOfLines={1}>{title}</Text>
<Text style={[styles.message, { color: themes[theme].titleText }]} numberOfLines={1}>{text}</Text>
</View>
</>
</Touchable>
<Touchable
onPress={hideNotification}
hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()}
>
<CustomIcon name='Cross' style={[styles.close, { color: themes[theme].titleText }]} size={20} />
</Touchable>
</View>
);
});
NotifierComponent.propTypes = {
baseUrl: PropTypes.string,
user: PropTypes.object,
notification: PropTypes.object,
isMasterDetail: PropTypes.bool
};
const mapStateToProps = state => ({
user: getUserSelector(state),
baseUrl: state.server.server,
isMasterDetail: state.app.isMasterDetail
});
export default connect(mapStateToProps)(NotifierComponent);

View File

@ -0,0 +1,40 @@
import React, { memo, useEffect } from 'react';
import { NotifierRoot, Notifier, Easing } from 'react-native-notifier';
import NotifierComponent from './NotifierComponent';
import EventEmitter from '../../utils/events';
import Navigation from '../../lib/Navigation';
import { getActiveRoute } from '../../utils/navigation';
export const INAPP_NOTIFICATION_EMITTER = 'NotificationInApp';
const InAppNotification = memo(() => {
const show = (notification) => {
const { payload } = notification;
const state = Navigation.navigationRef.current?.getRootState();
const route = getActiveRoute(state);
if (payload.rid) {
if (route?.name === 'RoomView' && route.params?.rid === payload.rid) {
return;
}
Notifier.showNotification({
showEasing: Easing.inOut(Easing.quad),
Component: NotifierComponent,
componentProps: {
notification
}
});
}
};
useEffect(() => {
EventEmitter.addEventListener(INAPP_NOTIFICATION_EMITTER, show);
return () => {
EventEmitter.removeListener(INAPP_NOTIFICATION_EMITTER);
};
}, []);
return <NotifierRoot />;
});
export default InAppNotification;

View File

@ -5,7 +5,6 @@ import {
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import { withNavigation } from 'react-navigation';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
@ -361,4 +360,4 @@ const mapDispatchToProps = dispatch => ({
loginRequest: params => dispatch(loginRequestAction(params)) loginRequest: params => dispatch(loginRequestAction(params))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(withNavigation(LoginServices))); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(LoginServices));

View File

@ -1,456 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Clipboard, Share } from 'react-native';
import { connect } from 'react-redux';
import ActionSheet from 'react-native-action-sheet';
import moment from 'moment';
import * as Haptics from 'expo-haptics';
import RocketChat from '../lib/rocketchat';
import database from '../lib/database';
import I18n from '../i18n';
import log from '../utils/log';
import Navigation from '../lib/Navigation';
import { getMessageTranslation } from './message/utils';
import { LISTENER } from './Toast';
import EventEmitter from '../utils/events';
import { showConfirmationAlert } from '../utils/info';
class MessageActions extends React.Component {
static propTypes = {
actionsHide: PropTypes.func.isRequired,
room: PropTypes.object.isRequired,
message: PropTypes.object,
user: PropTypes.object,
editInit: PropTypes.func.isRequired,
reactionInit: PropTypes.func.isRequired,
replyInit: PropTypes.func.isRequired,
isReadOnly: PropTypes.bool,
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,
Message_Read_Receipt_Store_Users: PropTypes.bool
};
constructor(props) {
super(props);
this.handleActionPress = this.handleActionPress.bind(this);
}
async componentDidMount() {
await this.setPermissions();
const {
Message_AllowStarring, Message_AllowPinning, Message_Read_Receipt_Store_Users, user, room, message, isReadOnly
} = this.props;
// Cancel
this.options = [I18n.t('Cancel')];
this.CANCEL_INDEX = 0;
// Reply
if (!isReadOnly) {
this.options.push(I18n.t('Reply'));
this.REPLY_INDEX = this.options.length - 1;
}
// Edit
if (this.allowEdit(this.props)) {
this.options.push(I18n.t('Edit'));
this.EDIT_INDEX = this.options.length - 1;
}
// Create Discussion
this.options.push(I18n.t('Create_Discussion'));
this.CREATE_DISCUSSION_INDEX = this.options.length - 1;
// Mark as unread
if (message.u && message.u._id !== user.id) {
this.options.push(I18n.t('Mark_unread'));
this.UNREAD_INDEX = this.options.length - 1;
}
// Permalink
this.options.push(I18n.t('Permalink'));
this.PERMALINK_INDEX = this.options.length - 1;
// Copy
this.options.push(I18n.t('Copy'));
this.COPY_INDEX = this.options.length - 1;
// Share
this.options.push(I18n.t('Share'));
this.SHARE_INDEX = this.options.length - 1;
// Quote
if (!isReadOnly) {
this.options.push(I18n.t('Quote'));
this.QUOTE_INDEX = this.options.length - 1;
}
// Star
if (Message_AllowStarring) {
this.options.push(I18n.t(message.starred ? 'Unstar' : 'Star'));
this.STAR_INDEX = this.options.length - 1;
}
// Pin
if (Message_AllowPinning) {
this.options.push(I18n.t(message.pinned ? 'Unpin' : 'Pin'));
this.PIN_INDEX = this.options.length - 1;
}
// Reaction
if (!isReadOnly || this.canReactWhenReadOnly()) {
this.options.push(I18n.t('Add_Reaction'));
this.REACTION_INDEX = this.options.length - 1;
}
// Read Receipts
if (Message_Read_Receipt_Store_Users) {
this.options.push(I18n.t('Read_Receipt'));
this.READ_RECEIPT_INDEX = this.options.length - 1;
}
// Toggle Auto-translate
if (room.autoTranslate && message.u && message.u._id !== user.id) {
this.options.push(I18n.t(message.autoTranslate ? 'View_Original' : 'Translate'));
this.TOGGLE_TRANSLATION_INDEX = this.options.length - 1;
}
// Report
this.options.push(I18n.t('Report'));
this.REPORT_INDEX = this.options.length - 1;
// Delete
if (this.allowDelete(this.props)) {
this.options.push(I18n.t('Delete'));
this.DELETE_INDEX = this.options.length - 1;
}
setTimeout(() => {
this.showActionSheet();
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
});
}
async setPermissions() {
try {
const { room } = this.props;
const permissions = ['edit-message', 'delete-message', 'force-delete-message'];
const result = await RocketChat.hasPermission(permissions, room.rid);
this.hasEditPermission = result[permissions[0]];
this.hasDeletePermission = result[permissions[1]];
this.hasForceDeletePermission = result[permissions[2]];
} catch (e) {
log(e);
}
Promise.resolve();
}
showActionSheet = () => {
ActionSheet.showActionSheetWithOptions({
options: this.options,
cancelButtonIndex: this.CANCEL_INDEX,
destructiveButtonIndex: this.DELETE_INDEX,
title: I18n.t('Message_actions')
}, (actionIndex) => {
this.handleActionPress(actionIndex);
});
}
getPermalink = async(message) => {
try {
return await RocketChat.getPermalinkMessage(message);
} catch (error) {
return null;
}
}
isOwn = props => props.message.u && props.message.u._id === props.user.id;
canReactWhenReadOnly = () => {
const { room } = this.props;
return room.reactWhenReadOnly;
}
allowEdit = (props) => {
if (props.isReadOnly) {
return false;
}
const editOwn = this.isOwn(props);
const { Message_AllowEditing: isEditAllowed, Message_AllowEditing_BlockEditInMinutes } = this.props;
if (!(this.hasEditPermission || (isEditAllowed && editOwn))) {
return false;
}
const blockEditInMinutes = Message_AllowEditing_BlockEditInMinutes;
if (blockEditInMinutes) {
let msgTs;
if (props.message.ts != null) {
msgTs = moment(props.message.ts);
}
let currentTsDiff;
if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes');
}
return currentTsDiff < blockEditInMinutes;
}
return true;
}
allowDelete = (props) => {
if (props.isReadOnly) {
return false;
}
// Prevent from deleting thread start message when positioned inside the thread
if (props.tmid && props.tmid === props.message.id) {
return false;
}
const deleteOwn = this.isOwn(props);
const { Message_AllowDeleting: isDeleteAllowed, Message_AllowDeleting_BlockDeleteInMinutes } = this.props;
if (!(this.hasDeletePermission || (isDeleteAllowed && deleteOwn) || this.hasForceDeletePermission)) {
return false;
}
if (this.hasForceDeletePermission) {
return true;
}
const blockDeleteInMinutes = Message_AllowDeleting_BlockDeleteInMinutes;
if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) {
let msgTs;
if (props.message.ts != null) {
msgTs = moment(props.message.ts);
}
let currentTsDiff;
if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes');
}
return currentTsDiff < blockDeleteInMinutes;
}
return true;
}
handleDelete = () => {
showConfirmationAlert({
message: I18n.t('You_will_not_be_able_to_recover_this_message'),
callToAction: I18n.t('Delete'),
onPress: async() => {
const { message } = this.props;
try {
await RocketChat.deleteMessage(message.id, message.subscription.id);
} catch (e) {
log(e);
}
}
});
}
handleEdit = () => {
const { message, editInit } = this.props;
editInit(message);
}
handleUnread = async() => {
const { message, room } = this.props;
const { id: messageId, ts } = message;
const { rid } = room;
try {
const db = database.active;
const result = await RocketChat.markAsUnread({ messageId });
if (result.success) {
const subCollection = db.collections.get('subscriptions');
const subRecord = await subCollection.find(rid);
await db.action(async() => {
try {
await subRecord.update(sub => sub.lastOpen = ts);
} catch {
// do nothing
}
});
Navigation.navigate('RoomsListView');
}
} catch (e) {
log(e);
}
}
handleCopy = async() => {
const { message } = this.props;
await Clipboard.setString(message.msg);
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
}
handleShare = async() => {
const { message } = this.props;
const permalink = await this.getPermalink(message);
if (!permalink) {
return;
}
Share.share({
message: permalink
});
};
handleStar = async() => {
const { message } = this.props;
try {
await RocketChat.toggleStarMessage(message.id, message.starred);
EventEmitter.emit(LISTENER, { message: message.starred ? I18n.t('Message_unstarred') : I18n.t('Message_starred') });
} catch (e) {
log(e);
}
}
handlePermalink = async() => {
const { message } = this.props;
const permalink = await this.getPermalink(message);
Clipboard.setString(permalink);
EventEmitter.emit(LISTENER, { message: I18n.t('Permalink_copied_to_clipboard') });
}
handlePin = async() => {
const { message } = this.props;
try {
await RocketChat.togglePinMessage(message.id, message.pinned);
} catch (e) {
log(e);
}
}
handleReply = () => {
const { message, replyInit } = this.props;
replyInit(message, true);
}
handleQuote = () => {
const { message, replyInit } = this.props;
replyInit(message, false);
}
handleReaction = () => {
const { message, reactionInit } = this.props;
reactionInit(message);
}
handleReadReceipt = () => {
const { message } = this.props;
Navigation.navigate('ReadReceiptsView', { messageId: message.id });
}
handleReport = async() => {
const { message } = this.props;
try {
await RocketChat.reportMessage(message.id);
Alert.alert(I18n.t('Message_Reported'));
} catch (e) {
log(e);
}
}
handleToggleTranslation = async() => {
const { message, room } = this.props;
try {
const db = database.active;
await db.action(async() => {
await message.update((m) => {
m.autoTranslate = !m.autoTranslate;
m._updatedAt = new Date();
});
});
const translatedMessage = getMessageTranslation(message, room.autoTranslateLanguage);
if (!translatedMessage) {
const m = {
_id: message.id,
rid: message.subscription.id,
u: message.u,
msg: message.msg
};
await RocketChat.translateMessage(m, room.autoTranslateLanguage);
}
} catch (e) {
log(e);
}
}
handleCreateDiscussion = () => {
const { message, room: channel } = this.props;
Navigation.navigate('CreateDiscussionView', { message, channel });
}
handleActionPress = (actionIndex) => {
if (actionIndex) {
switch (actionIndex) {
case this.REPLY_INDEX:
this.handleReply();
break;
case this.EDIT_INDEX:
this.handleEdit();
break;
case this.UNREAD_INDEX:
this.handleUnread();
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.REPORT_INDEX:
this.handleReport();
break;
case this.DELETE_INDEX:
this.handleDelete();
break;
case this.READ_RECEIPT_INDEX:
this.handleReadReceipt();
break;
case this.CREATE_DISCUSSION_INDEX:
this.handleCreateDiscussion();
break;
case this.TOGGLE_TRANSLATION_INDEX:
this.handleToggleTranslation();
break;
default:
break;
}
}
const { actionsHide } = this.props;
actionsHide();
}
render() {
return (
null
);
}
}
const mapStateToProps = state => ({
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,
Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users
});
export default connect(mapStateToProps)(MessageActions);

View File

@ -0,0 +1,140 @@
import React, { useEffect, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import {
View, Text, FlatList, StyleSheet
} from 'react-native';
import { withTheme } from '../../theme';
import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
import CustomEmoji from '../EmojiPicker/CustomEmoji';
import database from '../../lib/database';
import { Button } from '../ActionSheet';
import { useDimensions } from '../../dimensions';
export const HEADER_HEIGHT = 36;
const styles = StyleSheet.create({
container: {
alignItems: 'center',
marginHorizontal: 8
},
headerItem: {
height: 36,
width: 36,
borderRadius: 20,
marginHorizontal: 8,
justifyContent: 'center',
alignItems: 'center'
},
headerIcon: {
textAlign: 'center',
fontSize: 20,
color: '#fff'
},
customEmoji: {
height: 20,
width: 20
}
});
const keyExtractor = item => item?.id || item;
const DEFAULT_EMOJIS = ['clap', '+1', 'heart_eyes', 'grinning', 'thinking_face', 'smiley'];
const HeaderItem = React.memo(({
item, onReaction, server, theme
}) => (
<Button
testID={`message-actions-emoji-${ item.content || item }`}
onPress={() => onReaction({ emoji: `:${ item.content || item }:` })}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
theme={theme}
>
{item?.isCustom ? (
<CustomEmoji style={styles.customEmoji} emoji={item} baseUrl={server} />
) : (
<Text style={styles.headerIcon}>
{shortnameToUnicode(`:${ item.content || item }:`)}
</Text>
)}
</Button>
));
HeaderItem.propTypes = {
item: PropTypes.string,
onReaction: PropTypes.func,
server: PropTypes.string,
theme: PropTypes.string
};
const HeaderFooter = React.memo(({ onReaction, theme }) => (
<Button
testID='add-reaction'
onPress={onReaction}
style={[styles.headerItem, { backgroundColor: themes[theme].auxiliaryBackground }]}
theme={theme}
>
<CustomIcon name='add-reaction' size={24} color={themes[theme].bodyText} />
</Button>
));
HeaderFooter.propTypes = {
onReaction: PropTypes.func,
theme: PropTypes.string
};
const Header = React.memo(({
handleReaction, server, message, theme
}) => {
const [items, setItems] = useState([]);
const { width, height } = useDimensions();
const setEmojis = async() => {
try {
const db = database.active;
const freqEmojiCollection = db.collections.get('frequently_used_emojis');
let freqEmojis = await freqEmojiCollection.query().fetch();
const isLandscape = width > height;
const size = isLandscape ? width / 2 : width;
const quantity = (size / 50) - 1;
freqEmojis = freqEmojis.concat(DEFAULT_EMOJIS).slice(0, quantity);
setItems(freqEmojis);
} catch {
// Do nothing
}
};
useEffect(() => {
setEmojis();
}, []);
const onReaction = ({ emoji }) => handleReaction(emoji, message);
const renderItem = useCallback(({ item }) => <HeaderItem item={item} onReaction={onReaction} server={server} theme={theme} />);
const renderFooter = useCallback(() => <HeaderFooter onReaction={onReaction} theme={theme} />);
return (
<View style={[styles.container, { backgroundColor: themes[theme].focusedBackground }]}>
<FlatList
data={items}
renderItem={renderItem}
ListFooterComponent={renderFooter}
style={{ backgroundColor: themes[theme].focusedBackground }}
keyExtractor={keyExtractor}
showsHorizontalScrollIndicator={false}
scrollEnabled={false}
horizontal
/>
</View>
);
});
Header.propTypes = {
handleReaction: PropTypes.func,
server: PropTypes.string,
message: PropTypes.object,
theme: PropTypes.string
};
export default withTheme(Header);

View File

@ -0,0 +1,418 @@
import React, { forwardRef, useImperativeHandle } from 'react';
import PropTypes from 'prop-types';
import { Alert, Clipboard, Share } from 'react-native';
import { connect } from 'react-redux';
import moment from 'moment';
import RocketChat from '../../lib/rocketchat';
import database from '../../lib/database';
import I18n from '../../i18n';
import log from '../../utils/log';
import Navigation from '../../lib/Navigation';
import { getMessageTranslation } from '../message/utils';
import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events';
import { showConfirmationAlert } from '../../utils/info';
import { useActionSheet } from '../ActionSheet';
import Header, { HEADER_HEIGHT } from './Header';
const MessageActions = React.memo(forwardRef(({
room,
tmid,
user,
editInit,
reactionInit,
onReactionPress,
replyInit,
isReadOnly,
server,
Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes,
Message_AllowEditing,
Message_AllowEditing_BlockEditInMinutes,
Message_AllowPinning,
Message_AllowStarring,
Message_Read_Receipt_Store_Users
}, ref) => {
let permissions = {};
const { showActionSheet, hideActionSheet } = useActionSheet();
const getPermissions = async() => {
try {
const permission = ['edit-message', 'delete-message', 'force-delete-message', 'pin-message'];
const result = await RocketChat.hasPermission(permission, room.rid);
permissions = {
hasEditPermission: result[permission[0]],
hasDeletePermission: result[permission[1]],
hasForceDeletePermission: result[permission[2]],
hasPinPermission: result[permission[3]]
};
} catch {
// Do nothing
}
};
const isOwn = message => message.u && message.u._id === user.id;
const allowEdit = (message) => {
if (isReadOnly) {
return false;
}
const editOwn = isOwn(message);
if (!(permissions.hasEditPermission || (Message_AllowEditing && editOwn))) {
return false;
}
const blockEditInMinutes = Message_AllowEditing_BlockEditInMinutes;
if (blockEditInMinutes) {
let msgTs;
if (message.ts != null) {
msgTs = moment(message.ts);
}
let currentTsDiff;
if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes');
}
return currentTsDiff < blockEditInMinutes;
}
return true;
};
const allowDelete = (message) => {
if (isReadOnly) {
return false;
}
// Prevent from deleting thread start message when positioned inside the thread
if (tmid === message.id) {
return false;
}
const deleteOwn = isOwn(message);
if (!(permissions.hasDeletePermission || (Message_AllowDeleting && deleteOwn) || permissions.hasForceDeletePermission)) {
return false;
}
if (permissions.hasForceDeletePermission) {
return true;
}
const blockDeleteInMinutes = Message_AllowDeleting_BlockDeleteInMinutes;
if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) {
let msgTs;
if (message.ts != null) {
msgTs = moment(message.ts);
}
let currentTsDiff;
if (msgTs != null) {
currentTsDiff = moment().diff(msgTs, 'minutes');
}
return currentTsDiff < blockDeleteInMinutes;
}
return true;
};
const getPermalink = message => RocketChat.getPermalinkMessage(message);
const handleReply = message => replyInit(message, true);
const handleEdit = message => editInit(message);
const handleCreateDiscussion = (message) => {
Navigation.navigate('CreateDiscussionView', { message, channel: room });
};
const handleUnread = async(message) => {
const { id: messageId, ts } = message;
const { rid } = room;
try {
const db = database.active;
const result = await RocketChat.markAsUnread({ messageId });
if (result.success) {
const subCollection = db.collections.get('subscriptions');
const subRecord = await subCollection.find(rid);
await db.action(async() => {
try {
await subRecord.update(sub => sub.lastOpen = ts);
} catch {
// do nothing
}
});
Navigation.navigate('RoomsListView');
}
} catch (e) {
log(e);
}
};
const handlePermalink = async(message) => {
try {
const permalink = await getPermalink(message);
Clipboard.setString(permalink);
EventEmitter.emit(LISTENER, { message: I18n.t('Permalink_copied_to_clipboard') });
} catch {
// Do nothing
}
};
const handleCopy = async(message) => {
await Clipboard.setString(message.msg);
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
};
const handleShare = async(message) => {
try {
const permalink = await getPermalink(message);
Share.share({ message: permalink });
} catch {
// Do nothing
}
};
const handleQuote = message => replyInit(message, false);
const handleStar = async(message) => {
try {
await RocketChat.toggleStarMessage(message.id, message.starred);
EventEmitter.emit(LISTENER, { message: message.starred ? I18n.t('Message_unstarred') : I18n.t('Message_starred') });
} catch (e) {
log(e);
}
};
const handlePin = async(message) => {
try {
await RocketChat.togglePinMessage(message.id, message.pinned);
} catch (e) {
log(e);
}
};
const handleReaction = (shortname, message) => {
if (shortname) {
onReactionPress(shortname, message.id);
} else {
reactionInit(message);
}
// close actionSheet when click at header
hideActionSheet();
};
const handleReadReceipt = message => Navigation.navigate('ReadReceiptsView', { messageId: message.id });
const handleToggleTranslation = async(message) => {
try {
const db = database.active;
await db.action(async() => {
await message.update((m) => {
m.autoTranslate = !m.autoTranslate;
m._updatedAt = new Date();
});
});
const translatedMessage = getMessageTranslation(message, room.autoTranslateLanguage);
if (!translatedMessage) {
const m = {
_id: message.id,
rid: message.subscription.id,
u: message.u,
msg: message.msg
};
await RocketChat.translateMessage(m, room.autoTranslateLanguage);
}
} catch (e) {
log(e);
}
};
const handleReport = async(message) => {
try {
await RocketChat.reportMessage(message.id);
Alert.alert(I18n.t('Message_Reported'));
} catch (e) {
log(e);
}
};
const handleDelete = (message) => {
showConfirmationAlert({
message: I18n.t('You_will_not_be_able_to_recover_this_message'),
callToAction: I18n.t('Delete'),
onPress: async() => {
try {
await RocketChat.deleteMessage(message.id, message.subscription.id);
} catch (e) {
log(e);
}
}
});
};
const getOptions = (message) => {
let options = [];
// Reply
if (!isReadOnly) {
options = [{
title: I18n.t('Reply_in_Thread'),
icon: 'threads',
onPress: () => handleReply(message)
}];
}
// Quote
if (!isReadOnly) {
options.push({
title: I18n.t('Quote'),
icon: 'quote',
onPress: () => handleQuote(message)
});
}
// Edit
if (allowEdit(message)) {
options.push({
title: I18n.t('Edit'),
icon: 'edit',
onPress: () => handleEdit(message)
});
}
// Permalink
options.push({
title: I18n.t('Permalink'),
icon: 'link',
onPress: () => handlePermalink(message)
});
// Create Discussion
options.push({
title: I18n.t('Start_a_Discussion'),
icon: 'chat',
onPress: () => handleCreateDiscussion(message)
});
// Mark as unread
if (message.u && message.u._id !== user.id) {
options.push({
title: I18n.t('Mark_unread'),
icon: 'flag',
onPress: () => handleUnread(message)
});
}
// Copy
options.push({
title: I18n.t('Copy'),
icon: 'copy',
onPress: () => handleCopy(message)
});
// Share
options.push({
title: I18n.t('Share'),
icon: 'share',
onPress: () => handleShare(message)
});
// Star
if (Message_AllowStarring) {
options.push({
title: I18n.t(message.starred ? 'Unstar' : 'Star'),
icon: message.starred ? 'star-filled' : 'star',
onPress: () => handleStar(message)
});
}
// Pin
if (Message_AllowPinning && permissions?.hasPinPermission) {
options.push({
title: I18n.t(message.pinned ? 'Unpin' : 'Pin'),
icon: 'pin',
onPress: () => handlePin(message)
});
}
// Read Receipts
if (Message_Read_Receipt_Store_Users) {
options.push({
title: I18n.t('Read_Receipt'),
icon: 'info',
onPress: () => handleReadReceipt(message)
});
}
// Toggle Auto-translate
if (room.autoTranslate && message.u && message.u._id !== user.id) {
options.push({
title: I18n.t(message.autoTranslate ? 'View_Original' : 'Translate'),
icon: 'language',
onPress: () => handleToggleTranslation(message)
});
}
// Report
options.push({
title: I18n.t('Report'),
icon: 'warning',
danger: true,
onPress: () => handleReport(message)
});
// Delete
if (allowDelete(message)) {
options.push({
title: I18n.t('Delete'),
icon: 'trash',
danger: true,
onPress: () => handleDelete(message)
});
}
return options;
};
const showMessageActions = async(message) => {
await getPermissions();
showActionSheet({
options: getOptions(message),
headerHeight: HEADER_HEIGHT,
customHeader: (!isReadOnly || room.reactWhenReadOnly ? (
<Header
server={server}
handleReaction={handleReaction}
message={message}
/>
) : null)
});
};
useImperativeHandle(ref, () => ({ showMessageActions }));
}));
MessageActions.propTypes = {
room: PropTypes.object,
tmid: PropTypes.string,
user: PropTypes.object,
editInit: PropTypes.func,
reactionInit: PropTypes.func,
onReactionPress: PropTypes.func,
replyInit: PropTypes.func,
isReadOnly: PropTypes.bool,
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,
Message_Read_Receipt_Store_Users: PropTypes.bool,
server: PropTypes.string
};
const mapStateToProps = state => ({
server: state.server.server,
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,
Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users
});
export default connect(mapStateToProps, null, null, { forwardRef: true })(MessageActions);

View File

@ -32,7 +32,7 @@ const Item = ({ item, theme }) => {
{ loading ? <ActivityIndicator theme={theme} /> : null } { loading ? <ActivityIndicator theme={theme} /> : null }
</FastImage> </FastImage>
) )
: <CustomIcon name='file-generic' size={36} color={themes[theme].actionTintColor} /> : <CustomIcon name='clip' size={36} color={themes[theme].actionTintColor} />
} }
</TouchableOpacity> </TouchableOpacity>
); );

View File

@ -1,22 +1,28 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View } from 'react-native';
import { CancelEditingButton, ActionsButton } from './buttons'; import { CancelEditingButton, ActionsButton } from './buttons';
import styles from './styles';
const LeftButtons = React.memo(({ const LeftButtons = React.memo(({
theme, showMessageBoxActions, editing, editCancel theme, showMessageBoxActions, editing, editCancel, isActionsEnabled
}) => { }) => {
if (editing) { if (editing) {
return <CancelEditingButton onPress={editCancel} theme={theme} />; return <CancelEditingButton onPress={editCancel} theme={theme} />;
} }
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />; if (isActionsEnabled) {
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
}
return <View style={styles.buttonsWhitespace} />;
}); });
LeftButtons.propTypes = { LeftButtons.propTypes = {
theme: PropTypes.string, theme: PropTypes.string,
showMessageBoxActions: PropTypes.func.isRequired, showMessageBoxActions: PropTypes.func.isRequired,
editing: PropTypes.bool, editing: PropTypes.bool,
editCancel: PropTypes.func.isRequired editCancel: PropTypes.func.isRequired,
isActionsEnabled: PropTypes.bool
}; };
export default LeftButtons; export default LeftButtons;

View File

@ -1,18 +1,19 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
View, SafeAreaView, PermissionsAndroid, Text View, PermissionsAndroid, Text
} from 'react-native'; } from 'react-native';
import { AudioRecorder, AudioUtils } from 'react-native-audio'; import { AudioRecorder, AudioUtils } from 'react-native-audio';
import { BorderlessButton } from 'react-native-gesture-handler'; import { BorderlessButton } from 'react-native-gesture-handler';
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake'; import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
import RNFetchBlob from 'rn-fetch-blob'; import * as FileSystem from 'expo-file-system';
import styles from './styles'; import styles from './styles';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { isIOS, isAndroid } from '../../utils/deviceInfo'; import { isIOS, isAndroid } from '../../utils/deviceInfo';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import SafeAreaView from '../SafeAreaView';
export const _formatTime = function(seconds) { export const _formatTime = function(seconds) {
let minutes = Math.floor(seconds / 60); let minutes = Math.floor(seconds / 60);
@ -93,9 +94,6 @@ export default class extends React.PureComponent {
if (!didSucceed) { if (!didSucceed) {
return onFinish && onFinish(didSucceed); return onFinish && onFinish(didSucceed);
} }
if (isAndroid) {
filePath = filePath.startsWith('file://') ? filePath : `file://${ filePath }`;
}
const fileInfo = { const fileInfo = {
name: this.name, name: this.name,
mime: 'audio/aac', mime: 'audio/aac',
@ -110,9 +108,10 @@ export default class extends React.PureComponent {
finishAudioMessage = async() => { finishAudioMessage = async() => {
try { try {
this.recording = false; this.recording = false;
const filePath = await AudioRecorder.stopRecording(); let filePath = await AudioRecorder.stopRecording();
if (isAndroid) { if (isAndroid) {
const data = await RNFetchBlob.fs.stat(decodeURIComponent(filePath)); filePath = filePath.startsWith('file://') ? filePath : `file://${ filePath }`;
const data = await FileSystem.getInfoAsync(decodeURIComponent(filePath), { size: true });
this.finishRecording(true, filePath, data.size); this.finishRecording(true, filePath, data.size);
} }
} catch (err) { } catch (err) {
@ -134,6 +133,7 @@ export default class extends React.PureComponent {
return ( return (
<SafeAreaView <SafeAreaView
testID='messagebox-recording' testID='messagebox-recording'
theme={theme}
style={[ style={[
styles.textBox, styles.textBox,
{ borderTopColor: themes[theme].borderColor } { borderTopColor: themes[theme].borderColor }
@ -149,7 +149,7 @@ export default class extends React.PureComponent {
<CustomIcon <CustomIcon
size={22} size={22}
color={themes[theme].dangerColor} color={themes[theme].dangerColor}
name='cross' name='Cross'
/> />
</BorderlessButton> </BorderlessButton>
<Text key='currentTime' style={[styles.textBoxInput, { color: themes[theme].titleText }]}>{currentTime}</Text> <Text key='currentTime' style={[styles.textBoxInput, { color: themes[theme].titleText }]}>{currentTime}</Text>

View File

@ -3,6 +3,7 @@ import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import moment from 'moment'; import moment from 'moment';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import isEqual from 'lodash/isEqual';
import Markdown from '../markdown'; import Markdown from '../markdown';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
@ -58,7 +59,7 @@ const ReplyPreview = React.memo(({
> >
<View style={[styles.messageContainer, { backgroundColor: themes[theme].chatComponentBackground }]}> <View style={[styles.messageContainer, { backgroundColor: themes[theme].chatComponentBackground }]}>
<View style={styles.header}> <View style={styles.header}>
<Text style={[styles.username, { color: themes[theme].tintColor }]}>{message.u.username}</Text> <Text style={[styles.username, { color: themes[theme].tintColor }]}>{message.u?.username}</Text>
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text> <Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
</View> </View>
<Markdown <Markdown
@ -71,10 +72,10 @@ const ReplyPreview = React.memo(({
theme={theme} theme={theme}
/> />
</View> </View>
<CustomIcon name='cross' color={themes[theme].auxiliaryText} size={20} style={styles.close} onPress={close} /> <CustomIcon name='Cross' color={themes[theme].auxiliaryText} size={20} style={styles.close} onPress={close} />
</View> </View>
); );
}, (prevProps, nextProps) => prevProps.replying === nextProps.replying && prevProps.theme === nextProps.theme); }, (prevProps, nextProps) => prevProps.replying === nextProps.replying && prevProps.theme === nextProps.theme && isEqual(prevProps.message, nextProps.message));
ReplyPreview.propTypes = { ReplyPreview.propTypes = {
replying: PropTypes.bool, replying: PropTypes.bool,

View File

@ -1,23 +1,25 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View } from 'react-native';
import { SendButton, AudioButton, ActionsButton } from './buttons'; import { SendButton, AudioButton, ActionsButton } from './buttons';
import styles from './styles';
const RightButtons = React.memo(({ const RightButtons = React.memo(({
theme, showSend, submit, recordAudioMessage, recordAudioMessageEnabled, showMessageBoxActions theme, showSend, submit, recordAudioMessage, recordAudioMessageEnabled, showMessageBoxActions, isActionsEnabled
}) => { }) => {
if (showSend) { if (showSend) {
return <SendButton onPress={submit} theme={theme} />; return <SendButton onPress={submit} theme={theme} />;
} }
if (recordAudioMessageEnabled) { if (recordAudioMessageEnabled || isActionsEnabled) {
return ( return (
<> <>
<AudioButton onPress={recordAudioMessage} theme={theme} /> {recordAudioMessageEnabled ? <AudioButton onPress={recordAudioMessage} theme={theme} /> : null}
<ActionsButton onPress={showMessageBoxActions} theme={theme} /> {isActionsEnabled ? <ActionsButton onPress={showMessageBoxActions} theme={theme} /> : null}
</> </>
); );
} }
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />; return <View style={styles.buttonsWhitespace} />;
}); });
RightButtons.propTypes = { RightButtons.propTypes = {
@ -26,7 +28,8 @@ RightButtons.propTypes = {
submit: PropTypes.func.isRequired, submit: PropTypes.func.isRequired,
recordAudioMessage: PropTypes.func.isRequired, recordAudioMessage: PropTypes.func.isRequired,
recordAudioMessageEnabled: PropTypes.bool, recordAudioMessageEnabled: PropTypes.bool,
showMessageBoxActions: PropTypes.func.isRequired showMessageBoxActions: PropTypes.func.isRequired,
isActionsEnabled: PropTypes.bool
}; };
export default RightButtons; export default RightButtons;

View File

@ -1,255 +0,0 @@
import React, { Component } from 'react';
import {
View, Text, StyleSheet, Image, ScrollView, TouchableHighlight
} from 'react-native';
import PropTypes from 'prop-types';
import Modal from 'react-native-modal';
import { responsive } from 'react-native-responsive-ui';
import equal from 'deep-equal';
import TextInput from '../TextInput';
import Button from '../Button';
import I18n from '../../i18n';
import sharedStyles from '../../views/Styles';
import { isIOS } from '../../utils/deviceInfo';
import { themes } from '../../constants/colors';
import { CustomIcon } from '../../lib/Icons';
import { withTheme } from '../../theme';
import { withSplit } from '../../split';
const styles = StyleSheet.create({
modal: {
width: '100%',
alignItems: 'center',
margin: 0
},
titleContainer: {
flexDirection: 'row',
paddingHorizontal: 16,
paddingTop: 16
},
title: {
fontSize: 14,
...sharedStyles.textBold
},
container: {
height: 430,
flexDirection: 'column'
},
scrollView: {
flex: 1,
padding: 16
},
image: {
height: 150,
flex: 1,
marginBottom: 16,
resizeMode: 'contain'
},
bigPreview: {
height: 250
},
buttonContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
padding: 16
},
button: {
marginBottom: 0
},
androidButton: {
paddingHorizontal: 15,
justifyContent: 'center',
height: 48,
borderRadius: 2
},
androidButtonText: {
fontSize: 18,
textAlign: 'center'
},
fileIcon: {
margin: 20,
flex: 1,
textAlign: 'center'
},
video: {
flex: 1,
borderRadius: 4,
height: 150,
marginBottom: 6,
alignItems: 'center',
justifyContent: 'center'
}
});
class UploadModal extends Component {
static propTypes = {
isVisible: PropTypes.bool,
file: PropTypes.object,
close: PropTypes.func,
submit: PropTypes.func,
window: PropTypes.object,
theme: PropTypes.string,
split: PropTypes.bool
}
state = {
name: '',
description: '',
file: {}
};
static getDerivedStateFromProps(props, state) {
if (!equal(props.file, state.file) && props.file && props.file.path) {
return {
file: props.file,
name: props.file.filename || 'Filename',
description: ''
};
}
return null;
}
shouldComponentUpdate(nextProps, nextState) {
const { name, description, file } = this.state;
const {
window, isVisible, split, theme
} = this.props;
if (nextState.name !== name) {
return true;
}
if (nextProps.split !== split) {
return true;
}
if (nextProps.theme !== theme) {
return true;
}
if (nextState.description !== description) {
return true;
}
if (nextProps.isVisible !== isVisible) {
return true;
}
if (nextProps.window.width !== window.width) {
return true;
}
if (!equal(nextState.file, file)) {
return true;
}
return false;
}
submit = () => {
const { file, submit } = this.props;
const { name, description } = this.state;
submit({ ...file, name, description });
}
renderButtons = () => {
const { close, theme } = this.props;
if (isIOS) {
return (
<View style={[styles.buttonContainer, { backgroundColor: themes[theme].auxiliaryBackground }]}>
<Button
title={I18n.t('Cancel')}
type='secondary'
backgroundColor={themes[theme].chatComponentBackground}
style={styles.button}
onPress={close}
theme={theme}
/>
<Button
title={I18n.t('Send')}
type='primary'
style={styles.button}
onPress={this.submit}
theme={theme}
/>
</View>
);
}
// FIXME: RNGH don't work well on Android modals: https://github.com/kmagiera/react-native-gesture-handler/issues/139
return (
<View style={[styles.buttonContainer, { backgroundColor: themes[theme].auxiliaryBackground }]}>
<TouchableHighlight
onPress={close}
style={[styles.androidButton, { backgroundColor: themes[theme].chatComponentBackground }]}
underlayColor={themes[theme].chatComponentBackground}
activeOpacity={0.5}
>
<Text style={[styles.androidButtonText, { ...sharedStyles.textBold, color: themes[theme].tintColor }]}>{I18n.t('Cancel')}</Text>
</TouchableHighlight>
<TouchableHighlight
onPress={this.submit}
style={[styles.androidButton, { backgroundColor: themes[theme].tintColor }]}
underlayColor={themes[theme].tintColor}
activeOpacity={0.5}
>
<Text style={[styles.androidButtonText, { ...sharedStyles.textMedium, color: themes[theme].buttonText }]}>{I18n.t('Send')}</Text>
</TouchableHighlight>
</View>
);
}
renderPreview() {
const { file, split, theme } = this.props;
if (file.mime && file.mime.match(/image/)) {
return (<Image source={{ isStatic: true, uri: file.path }} style={[styles.image, split && styles.bigPreview]} />);
}
if (file.mime && file.mime.match(/video/)) {
return (
<View style={[styles.video, { backgroundColor: themes[theme].bannerBackground }]}>
<CustomIcon name='play' size={72} color={themes[theme].buttonText} />
</View>
);
}
return (<CustomIcon name='file-generic' size={72} style={[styles.fileIcon, { color: themes[theme].tintColor }]} />);
}
render() {
const {
window: { width }, isVisible, close, split, theme
} = this.props;
const { name, description } = this.state;
return (
<Modal
isVisible={isVisible}
style={styles.modal}
onBackdropPress={close}
onBackButtonPress={close}
animationIn='fadeIn'
animationOut='fadeOut'
useNativeDriver
hideModalContentWhileAnimating
avoidKeyboard
>
<View style={[styles.container, { width: width - 32, backgroundColor: themes[theme].chatComponentBackground }, split && [sharedStyles.modal, sharedStyles.modalFormSheet]]}>
<View style={styles.titleContainer}>
<Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('Upload_file_question_mark')}</Text>
</View>
<ScrollView style={styles.scrollView}>
{this.renderPreview()}
<TextInput
placeholder={I18n.t('File_name')}
value={name}
onChangeText={value => this.setState({ name: value })}
theme={theme}
/>
<TextInput
placeholder={I18n.t('File_description')}
value={description}
onChangeText={value => this.setState({ description: value })}
theme={theme}
/>
</ScrollView>
{this.renderButtons()}
</View>
</Modal>
);
}
}
export default responsive(withTheme(withSplit(UploadModal)));

View File

@ -17,7 +17,7 @@ const BaseButton = React.memo(({
accessibilityLabel={I18n.t(accessibilityLabel)} accessibilityLabel={I18n.t(accessibilityLabel)}
accessibilityTraits='button' accessibilityTraits='button'
> >
<CustomIcon name={icon} size={23} color={themes[theme].tintColor} /> <CustomIcon name={icon} size={25} color={themes[theme].tintColor} />
</BorderlessButton> </BorderlessButton>
)); ));

View File

@ -8,7 +8,7 @@ const CancelEditingButton = React.memo(({ theme, onPress }) => (
onPress={onPress} onPress={onPress}
testID='messagebox-cancel-editing' testID='messagebox-cancel-editing'
accessibilityLabel='Cancel_editing' accessibilityLabel='Cancel_editing'
icon='cross' icon='Cross'
theme={theme} theme={theme}
/> />
)); ));

View File

@ -8,7 +8,7 @@ const SendButton = React.memo(({ theme, onPress }) => (
onPress={onPress} onPress={onPress}
testID='messagebox-send-message' testID='messagebox-send-message'
accessibilityLabel='Send_message' accessibilityLabel='Send_message'
icon='Send-active' icon='send-active'
theme={theme} theme={theme}
/> />
)); ));

View File

@ -1,12 +1,13 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, Alert, Keyboard } from 'react-native'; import {
View, Alert, Keyboard, NativeModules
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { KeyboardAccessoryView } from 'react-native-keyboard-input'; import { KeyboardAccessoryView } from 'react-native-keyboard-input';
import ImagePicker from 'react-native-image-crop-picker'; import ImagePicker from 'react-native-image-crop-picker';
import equal from 'deep-equal'; import equal from 'deep-equal';
import DocumentPicker from 'react-native-document-picker'; import DocumentPicker from 'react-native-document-picker';
import ActionSheet from 'react-native-action-sheet';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import { generateTriggerId } from '../../lib/methods/actions'; import { generateTriggerId } from '../../lib/methods/actions';
@ -17,7 +18,6 @@ import styles from './styles';
import database from '../../lib/database'; import database from '../../lib/database';
import { emojis } from '../../emojis'; import { emojis } from '../../emojis';
import Recording from './Recording'; import Recording from './Recording';
import UploadModal from './UploadModal';
import log from '../../utils/log'; import log from '../../utils/log';
import I18n from '../../i18n'; import I18n from '../../i18n';
import ReplyPreview from './ReplyPreview'; import ReplyPreview from './ReplyPreview';
@ -43,9 +43,9 @@ import {
MENTIONS_TRACKING_TYPE_USERS MENTIONS_TRACKING_TYPE_USERS
} from './constants'; } from './constants';
import CommandsPreview from './CommandsPreview'; import CommandsPreview from './CommandsPreview';
import { Review } from '../../utils/review';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
import Navigation from '../../lib/Navigation'; import Navigation from '../../lib/Navigation';
import { withActionSheet } from '../ActionSheet';
const imagePickerConfig = { const imagePickerConfig = {
cropping: true, cropping: true,
@ -54,6 +54,7 @@ const imagePickerConfig = {
}; };
const libraryPickerConfig = { const libraryPickerConfig = {
multiple: true,
mediaType: 'any' mediaType: 'any'
}; };
@ -61,13 +62,6 @@ const videoPickerConfig = {
mediaType: 'video' mediaType: 'video'
}; };
const FILE_CANCEL_INDEX = 0;
const FILE_PHOTO_INDEX = 1;
const FILE_VIDEO_INDEX = 2;
const FILE_LIBRARY_INDEX = 3;
const FILE_DOCUMENT_INDEX = 4;
const CREATE_DISCUSSION_INDEX = 5;
class MessageBox extends Component { class MessageBox extends Component {
static propTypes = { static propTypes = {
rid: PropTypes.string.isRequired, rid: PropTypes.string.isRequired,
@ -95,7 +89,24 @@ class MessageBox extends Component {
typing: PropTypes.func, typing: PropTypes.func,
theme: PropTypes.string, theme: PropTypes.string,
replyCancel: PropTypes.func, replyCancel: PropTypes.func,
navigation: PropTypes.object showSend: PropTypes.bool,
navigation: PropTypes.object,
children: PropTypes.node,
isMasterDetail: PropTypes.bool,
showActionSheet: PropTypes.func,
iOSScrollBehavior: PropTypes.number,
sharing: PropTypes.bool,
isActionsEnabled: PropTypes.bool
}
static defaultProps = {
message: {
id: ''
},
sharing: false,
iOSScrollBehavior: NativeModules.KeyboardTrackingViewManager?.KeyboardTrackingScrollBehaviorFixedOffset,
isActionsEnabled: true,
getCustomEmoji: () => {}
} }
constructor(props) { constructor(props) {
@ -103,26 +114,45 @@ class MessageBox extends Component {
this.state = { this.state = {
mentions: [], mentions: [],
showEmojiKeyboard: false, showEmojiKeyboard: false,
showSend: false, showSend: props.showSend,
recording: false, recording: false,
trackingType: '', trackingType: '',
file: {
isVisible: false
},
commandPreview: [], commandPreview: [],
showCommandPreview: false, showCommandPreview: false,
command: {} command: {}
}; };
this.text = ''; this.text = '';
this.focused = false; this.focused = false;
this.messageBoxActions = [
I18n.t('Cancel'), // MessageBox Actions
I18n.t('Take_a_photo'), this.options = [
I18n.t('Take_a_video'), {
I18n.t('Choose_from_library'), title: I18n.t('Take_a_photo'),
I18n.t('Choose_file'), icon: 'image',
I18n.t('Create_Discussion') onPress: this.takePhoto
},
{
title: I18n.t('Take_a_video'),
icon: 'video-1',
onPress: this.takeVideo
},
{
title: I18n.t('Choose_from_library'),
icon: 'share',
onPress: this.chooseFromLibrary
},
{
title: I18n.t('Choose_file'),
icon: 'folder',
onPress: this.chooseFile
},
{
title: I18n.t('Create_Discussion'),
icon: 'chat',
onPress: this.createDiscussion
}
]; ];
const libPickerLabels = { const libPickerLabels = {
cropperChooseText: I18n.t('Choose'), cropperChooseText: I18n.t('Choose'),
cropperCancelText: I18n.t('Cancel'), cropperCancelText: I18n.t('Cancel'),
@ -144,27 +174,29 @@ class MessageBox extends Component {
async componentDidMount() { async componentDidMount() {
const db = database.active; const db = database.active;
const { rid, tmid, navigation } = this.props; const {
rid, tmid, navigation, sharing
} = this.props;
let msg; let msg;
try { try {
const threadsCollection = db.collections.get('threads'); const threadsCollection = db.collections.get('threads');
const subsCollection = db.collections.get('subscriptions'); const subsCollection = db.collections.get('subscriptions');
try {
this.room = await subsCollection.find(rid);
} catch (error) {
console.log('Messagebox.didMount: Room not found');
}
if (tmid) { if (tmid) {
try { try {
const thread = await threadsCollection.find(tmid); this.thread = await threadsCollection.find(tmid);
if (thread) { if (this.thread && !sharing) {
msg = thread.draftMessage; msg = this.thread.draftMessage;
} }
} catch (error) { } catch (error) {
console.log('Messagebox.didMount: Thread not found'); console.log('Messagebox.didMount: Thread not found');
} }
} else { } else if (!sharing) {
try { msg = this.room?.draftMessage;
this.room = await subsCollection.find(rid);
msg = this.room.draftMessage;
} catch (error) {
console.log('Messagebox.didMount: Room not found');
}
} }
} catch (e) { } catch (e) {
log(e); log(e);
@ -183,16 +215,25 @@ class MessageBox extends Component {
EventEmiter.addEventListener(KEY_COMMAND, this.handleCommands); EventEmiter.addEventListener(KEY_COMMAND, this.handleCommands);
} }
this.didFocusListener = navigation.addListener('didFocus', () => { this.unsubscribeFocus = navigation.addListener('focus', () => {
if (this.tracking && this.tracking.resetTracking) { if (this.tracking && this.tracking.resetTracking) {
this.tracking.resetTracking(); this.tracking.resetTracking();
} }
}); });
this.unsubscribeBlur = navigation.addListener('blur', () => {
this.component?.blur();
});
} }
UNSAFE_componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
const { isFocused, editing, replying } = this.props; const {
if (!isFocused()) { isFocused, editing, replying, sharing
} = this.props;
if (!isFocused?.()) {
return;
}
if (sharing) {
this.setInput(nextProps.message.msg ?? '');
return; return;
} }
if (editing !== nextProps.editing && nextProps.editing) { if (editing !== nextProps.editing && nextProps.editing) {
@ -200,6 +241,7 @@ class MessageBox extends Component {
if (this.text) { if (this.text) {
this.setShowSend(true); this.setShowSend(true);
} }
this.focus();
} else if (replying !== nextProps.replying && nextProps.replying) { } else if (replying !== nextProps.replying && nextProps.replying) {
this.focus(); this.focus();
} else if (!nextProps.message) { } else if (!nextProps.message) {
@ -209,11 +251,11 @@ class MessageBox extends Component {
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { const {
showEmojiKeyboard, showSend, recording, mentions, file, commandPreview showEmojiKeyboard, showSend, recording, mentions, commandPreview
} = this.state; } = this.state;
const { const {
roomType, replying, editing, isFocused, theme roomType, replying, editing, isFocused, message, theme, children
} = this.props; } = this.props;
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
return true; return true;
@ -245,7 +287,10 @@ class MessageBox extends Component {
if (!equal(nextState.commandPreview, commandPreview)) { if (!equal(nextState.commandPreview, commandPreview)) {
return true; return true;
} }
if (!equal(nextState.file, file)) { if (!equal(nextProps.message, message)) {
return true;
}
if (!equal(nextProps.children, children)) {
return true; return true;
} }
return false; return false;
@ -268,8 +313,11 @@ class MessageBox extends Component {
if (this.getSlashCommands && this.getSlashCommands.stop) { if (this.getSlashCommands && this.getSlashCommands.stop) {
this.getSlashCommands.stop(); this.getSlashCommands.stop();
} }
if (this.didFocusListener && this.didFocusListener.remove) { if (this.unsubscribeFocus) {
this.didFocusListener.remove(); this.unsubscribeFocus();
}
if (this.unsubscribeBlur) {
this.unsubscribeBlur();
} }
if (isTablet) { if (isTablet) {
EventEmiter.removeListener(KEY_COMMAND, this.handleCommands); EventEmiter.removeListener(KEY_COMMAND, this.handleCommands);
@ -285,22 +333,26 @@ class MessageBox extends Component {
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
debouncedOnChangeText = debounce(async(text) => { debouncedOnChangeText = debounce(async(text) => {
const { sharing } = this.props;
const db = database.active; const db = database.active;
const isTextEmpty = text.length === 0; const isTextEmpty = text.length === 0;
// this.setShowSend(!isTextEmpty); // this.setShowSend(!isTextEmpty);
this.handleTyping(!isTextEmpty); this.handleTyping(!isTextEmpty);
// matches if their is text that stats with '/' and group the command and params so we can use it "/command params"
const slashCommand = text.match(/^\/([a-z0-9._-]+) (.+)/im); if (!sharing) {
if (slashCommand) { // matches if their is text that stats with '/' and group the command and params so we can use it "/command params"
const [, name, params] = slashCommand; const slashCommand = text.match(/^\/([a-z0-9._-]+) (.+)/im);
const commandsCollection = db.collections.get('slash_commands'); if (slashCommand) {
try { const [, name, params] = slashCommand;
const command = await commandsCollection.find(name); const commandsCollection = db.collections.get('slash_commands');
if (command.providesPreview) { try {
return this.setCommandPreview(command, name, params); const command = await commandsCollection.find(name);
if (command.providesPreview) {
return this.setCommandPreview(command, name, params);
}
} catch (e) {
console.log('Slash command not found');
} }
} catch (e) {
console.log('Slash command not found');
} }
} }
@ -310,12 +362,20 @@ class MessageBox extends Component {
const cursor = Math.max(start, end); const cursor = Math.max(start, end);
const lastNativeText = this.component?.lastNativeText || ''; const lastNativeText = this.component?.lastNativeText || '';
// matches if text either starts with '/' or have (@,#,:) then it groups whatever comes next of mention type // matches if text either starts with '/' or have (@,#,:) then it groups whatever comes next of mention type
const regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im; let regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im;
// if sharing, track #|@|:
if (sharing) {
regexp = /(#|@|:)([a-z0-9._-]+)$/im;
}
const result = lastNativeText.substr(0, cursor).match(regexp); const result = lastNativeText.substr(0, cursor).match(regexp);
if (!result) { if (!result) {
const slash = lastNativeText.match(/^\/$/); // matches only '/' in input if (!sharing) {
if (slash) { const slash = lastNativeText.match(/^\/$/); // matches only '/' in input
return this.identifyMentionKeyword('', MENTIONS_TRACKING_TYPE_COMMANDS); if (slash) {
return this.identifyMentionKeyword('', MENTIONS_TRACKING_TYPE_COMMANDS);
}
} }
return this.stopTrackingMention(); return this.stopTrackingMention();
} }
@ -455,7 +515,10 @@ class MessageBox extends Component {
} }
handleTyping = (isTyping) => { handleTyping = (isTyping) => {
const { typing, rid } = this.props; const { typing, rid, sharing } = this.props;
if (sharing) {
return;
}
if (!isTyping) { if (!isTyping) {
if (this.typingTimeout) { if (this.typingTimeout) {
clearTimeout(this.typingTimeout); clearTimeout(this.typingTimeout);
@ -495,7 +558,8 @@ class MessageBox extends Component {
setShowSend = (showSend) => { setShowSend = (showSend) => {
const { showSend: prevShowSend } = this.state; const { showSend: prevShowSend } = this.state;
if (prevShowSend !== showSend) { const { showSend: propShowSend } = this.props;
if (prevShowSend !== showSend && !propShowSend) {
this.setState({ showSend }); this.setState({ showSend });
} }
} }
@ -507,7 +571,7 @@ class MessageBox extends Component {
canUploadFile = (file) => { canUploadFile = (file) => {
const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = this.props; const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = this.props;
const result = canUploadFile(file, { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize }); const result = canUploadFile(file, FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize);
if (result.success) { if (result.success) {
return true; return true;
} }
@ -515,33 +579,11 @@ class MessageBox extends Component {
return false; return false;
} }
sendMediaMessage = async(file) => {
const {
rid, tmid, baseUrl: server, user, message: { id: messageTmid }, replyCancel
} = this.props;
this.setState({ file: { isVisible: false } });
const fileInfo = {
name: file.name,
description: file.description,
size: file.size,
type: file.mime,
store: 'Uploads',
path: file.path
};
try {
replyCancel();
await RocketChat.sendFileMessage(rid, fileInfo, tmid || messageTmid, server, user);
Review.pushPositiveEvent();
} catch (e) {
log(e);
}
}
takePhoto = async() => { takePhoto = async() => {
try { try {
const image = await ImagePicker.openCamera(this.imagePickerConfig); const image = await ImagePicker.openCamera(this.imagePickerConfig);
if (this.canUploadFile(image)) { if (this.canUploadFile(image)) {
this.showUploadModal(image); this.openShareView([image]);
} }
} catch (e) { } catch (e) {
// Do nothing // Do nothing
@ -552,7 +594,7 @@ class MessageBox extends Component {
try { try {
const video = await ImagePicker.openCamera(this.videoPickerConfig); const video = await ImagePicker.openCamera(this.videoPickerConfig);
if (this.canUploadFile(video)) { if (this.canUploadFile(video)) {
this.showUploadModal(video); this.openShareView([video]);
} }
} catch (e) { } catch (e) {
// Do nothing // Do nothing
@ -561,10 +603,8 @@ class MessageBox extends Component {
chooseFromLibrary = async() => { chooseFromLibrary = async() => {
try { try {
const image = await ImagePicker.openPicker(this.libraryPickerConfig); const attachments = await ImagePicker.openPicker(this.libraryPickerConfig);
if (this.canUploadFile(image)) { this.openShareView(attachments);
this.showUploadModal(image);
}
} catch (e) { } catch (e) {
// Do nothing // Do nothing
} }
@ -582,7 +622,7 @@ class MessageBox extends Component {
path: res.uri path: res.uri
}; };
if (this.canUploadFile(file)) { if (this.canUploadFile(file)) {
this.showUploadModal(file); this.openShareView([file]);
} }
} catch (e) { } catch (e) {
if (!DocumentPicker.isCancel(e)) { if (!DocumentPicker.isCancel(e)) {
@ -591,43 +631,30 @@ class MessageBox extends Component {
} }
} }
createDiscussion = () => { openShareView = (attachments) => {
Navigation.navigate('CreateDiscussionView', { channel: this.room }); const { message, replyCancel, replyWithMention } = this.props;
// Start a thread with an attachment
let { thread } = this;
if (replyWithMention) {
thread = message;
replyCancel();
}
Navigation.navigate('ShareView', { room: this.room, thread, attachments });
} }
showUploadModal = (file) => { createDiscussion = () => {
this.setState({ file: { ...file, isVisible: true } }); const { isMasterDetail } = this.props;
const params = { channel: this.room, showCloseModal: true };
if (isMasterDetail) {
Navigation.navigate('ModalStackNavigator', { screen: 'CreateDiscussionView', params });
} else {
Navigation.navigate('NewMessageStackNavigator', { screen: 'CreateDiscussionView', params });
}
} }
showMessageBoxActions = () => { showMessageBoxActions = () => {
ActionSheet.showActionSheetWithOptions({ const { showActionSheet } = this.props;
options: this.messageBoxActions, showActionSheet({ options: this.options });
cancelButtonIndex: FILE_CANCEL_INDEX
}, (actionIndex) => {
this.handleMessageBoxActions(actionIndex);
});
}
handleMessageBoxActions = (actionIndex) => {
switch (actionIndex) {
case FILE_PHOTO_INDEX:
this.takePhoto();
break;
case FILE_VIDEO_INDEX:
this.takeVideo();
break;
case FILE_LIBRARY_INDEX:
this.chooseFromLibrary();
break;
case FILE_DOCUMENT_INDEX:
this.chooseFile();
break;
case CREATE_DISCUSSION_INDEX:
this.createDiscussion();
break;
default:
break;
}
} }
editCancel = () => { editCancel = () => {
@ -672,16 +699,22 @@ class MessageBox extends Component {
submit = async() => { submit = async() => {
const { const {
onSubmit, rid: roomId, tmid onSubmit, rid: roomId, tmid, showSend, sharing
} = this.props; } = this.props;
const message = this.text; const message = this.text;
// if sharing, only execute onSubmit prop
if (sharing) {
onSubmit(message);
return;
}
this.clearInput(); this.clearInput();
this.debouncedOnChangeText.stop(); this.debouncedOnChangeText.stop();
this.closeEmoji(); this.closeEmoji();
this.stopTrackingMention(); this.stopTrackingMention();
this.handleTyping(false); this.handleTyping(false);
if (message.trim() === '') { if (message.trim() === '' && !showSend) {
return; return;
} }
@ -802,7 +835,7 @@ class MessageBox extends Component {
recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview
} = this.state; } = this.state;
const { const {
editing, message, replying, replyCancel, user, getCustomEmoji, theme, Message_AudioRecorderEnabled editing, message, replying, replyCancel, user, getCustomEmoji, theme, Message_AudioRecorderEnabled, children, isActionsEnabled
} = this.props; } = this.props;
const isAndroidTablet = isTablet && isAndroid ? { const isAndroidTablet = isTablet && isAndroid ? {
@ -839,6 +872,7 @@ class MessageBox extends Component {
showEmojiKeyboard={showEmojiKeyboard} showEmojiKeyboard={showEmojiKeyboard}
editing={editing} editing={editing}
showMessageBoxActions={this.showMessageBoxActions} showMessageBoxActions={this.showMessageBoxActions}
isActionsEnabled={isActionsEnabled}
editCancel={this.editCancel} editCancel={this.editCancel}
openEmoji={this.openEmoji} openEmoji={this.openEmoji}
closeEmoji={this.closeEmoji} closeEmoji={this.closeEmoji}
@ -865,17 +899,21 @@ class MessageBox extends Component {
recordAudioMessage={this.recordAudioMessage} recordAudioMessage={this.recordAudioMessage}
recordAudioMessageEnabled={Message_AudioRecorderEnabled} recordAudioMessageEnabled={Message_AudioRecorderEnabled}
showMessageBoxActions={this.showMessageBoxActions} showMessageBoxActions={this.showMessageBoxActions}
isActionsEnabled={isActionsEnabled}
/> />
</View> </View>
</View> </View>
{children}
</> </>
); );
} }
render() { render() {
console.count(`${ this.constructor.name }.render calls`); console.count(`${ this.constructor.name }.render calls`);
const { showEmojiKeyboard, file } = this.state; const { showEmojiKeyboard } = this.state;
const { user, baseUrl, theme } = this.props; const {
user, baseUrl, theme, iOSScrollBehavior
} = this.props;
return ( return (
<MessageboxContext.Provider <MessageboxContext.Provider
value={{ value={{
@ -897,12 +935,7 @@ class MessageBox extends Component {
requiresSameParentToManageScrollView requiresSameParentToManageScrollView
addBottomView addBottomView
bottomViewColor={themes[theme].messageboxBackground} bottomViewColor={themes[theme].messageboxBackground}
/> iOSScrollBehavior={iOSScrollBehavior}
<UploadModal
isVisible={(file && file.isVisible)}
file={file}
close={() => this.setState({ file: {} })}
submit={this.sendMediaMessage}
/> />
</MessageboxContext.Provider> </MessageboxContext.Provider>
); );
@ -910,6 +943,7 @@ class MessageBox extends Component {
} }
const mapStateToProps = state => ({ const mapStateToProps = state => ({
isMasterDetail: state.app.isMasterDetail,
baseUrl: state.server.server, baseUrl: state.server.server,
threadsEnabled: state.settings.Threads_enabled, threadsEnabled: state.settings.Threads_enabled,
user: getUserSelector(state), user: getUserSelector(state),
@ -922,4 +956,4 @@ const dispatchToProps = ({
typing: (rid, status) => userTypingAction(rid, status) typing: (rid, status) => userTypingAction(rid, status)
}); });
export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(MessageBox); export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(withActionSheet(MessageBox));

View File

@ -103,5 +103,8 @@ export default StyleSheet.create({
}, },
scrollViewMention: { scrollViewMention: {
maxHeight: SCROLLVIEW_MENTION_HEIGHT maxHeight: SCROLLVIEW_MENTION_HEIGHT
},
buttonsWhitespace: {
width: 15
} }
}); });

View File

@ -1,41 +1,22 @@
import React from 'react'; import { useImperativeHandle, forwardRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ActionSheet from 'react-native-action-sheet';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/database'; import database from '../lib/database';
import protectedFunction from '../lib/methods/helpers/protectedFunction'; import protectedFunction from '../lib/methods/helpers/protectedFunction';
import { useActionSheet } from './ActionSheet';
import I18n from '../i18n'; import I18n from '../i18n';
import log from '../utils/log'; import log from '../utils/log';
class MessageErrorActions extends React.Component { const MessageErrorActions = forwardRef(({ tmid }, ref) => {
static propTypes = { const { showActionSheet } = useActionSheet();
actionsHide: PropTypes.func.isRequired,
message: PropTypes.object,
tmid: PropTypes.string
};
// eslint-disable-next-line react/sort-comp const handleResend = protectedFunction(async(message) => {
constructor(props) {
super(props);
this.handleActionPress = this.handleActionPress.bind(this);
this.options = [I18n.t('Cancel'), I18n.t('Delete'), I18n.t('Resend')];
this.CANCEL_INDEX = 0;
this.DELETE_INDEX = 1;
this.RESEND_INDEX = 2;
setTimeout(() => {
this.showActionSheet();
});
}
handleResend = protectedFunction(async() => {
const { message, tmid } = this.props;
await RocketChat.resendMessage(message, tmid); await RocketChat.resendMessage(message, tmid);
}); });
handleDelete = async() => { const handleDelete = async(message) => {
try { try {
const { message, tmid } = this.props;
const db = database.active; const db = database.active;
const deleteBatch = []; const deleteBatch = [];
const msgCollection = db.collections.get('messages'); const msgCollection = db.collections.get('messages');
@ -49,7 +30,7 @@ class MessageErrorActions extends React.Component {
try { try {
const msg = await msgCollection.find(message.id); const msg = await msgCollection.find(message.id);
deleteBatch.push(msg.prepareDestroyPermanently()); deleteBatch.push(msg.prepareDestroyPermanently());
} catch (error) { } catch {
// Do nothing: message not found // Do nothing: message not found
} }
@ -68,7 +49,7 @@ class MessageErrorActions extends React.Component {
// If the whole thread was removed, delete the thread // If the whole thread was removed, delete the thread
const thread = await threadCollection.find(tmid); const thread = await threadCollection.find(tmid);
deleteBatch.push(thread.prepareDestroyPermanently()); deleteBatch.push(thread.prepareDestroyPermanently());
} catch (error) { } catch {
// Do nothing: thread not found // Do nothing: thread not found
} }
} else { } else {
@ -78,7 +59,7 @@ class MessageErrorActions extends React.Component {
}) })
); );
} }
} catch (error) { } catch {
// Do nothing: message not found // Do nothing: message not found
} }
} }
@ -88,39 +69,34 @@ class MessageErrorActions extends React.Component {
} catch (e) { } catch (e) {
log(e); log(e);
} }
} };
showActionSheet = () => { const showMessageErrorActions = (message) => {
ActionSheet.showActionSheetWithOptions({ showActionSheet({
options: this.options, options: [
cancelButtonIndex: this.CANCEL_INDEX, {
destructiveButtonIndex: this.DELETE_INDEX, title: I18n.t('Resend'),
title: I18n.t('Message_actions') icon: 'send',
}, (actionIndex) => { onPress: () => handleResend(message)
this.handleActionPress(actionIndex); },
{
title: I18n.t('Delete'),
icon: 'trash',
danger: true,
onPress: () => handleDelete(message)
}
],
hasCancel: true
}); });
} };
handleActionPress = (actionIndex) => { useImperativeHandle(ref, () => ({
const { actionsHide } = this.props; showMessageErrorActions
switch (actionIndex) { }));
case this.RESEND_INDEX: });
this.handleResend(); MessageErrorActions.propTypes = {
break; message: PropTypes.object,
case this.DELETE_INDEX: tmid: PropTypes.string
this.handleDelete(); };
break;
default:
break;
}
actionsHide();
}
render() {
return (
null
);
}
}
export default MessageErrorActions; export default MessageErrorActions;

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { import {
View, Text, FlatList, StyleSheet, SafeAreaView View, Text, FlatList, StyleSheet
} from 'react-native'; } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
@ -12,8 +12,12 @@ import { CustomIcon } from '../lib/Icons';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import SafeAreaView from './SafeAreaView';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
safeArea: {
backgroundColor: 'transparent'
},
titleContainer: { titleContainer: {
alignItems: 'center', alignItems: 'center',
paddingVertical: 10 paddingVertical: 10
@ -95,12 +99,12 @@ const ModalContent = React.memo(({
}) => { }) => {
if (message && message.reactions) { if (message && message.reactions) {
return ( return (
<SafeAreaView style={{ flex: 1 }}> <SafeAreaView theme={props.theme} style={styles.safeArea}>
<Touchable onPress={onClose}> <Touchable onPress={onClose}>
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<CustomIcon <CustomIcon
style={[styles.closeButton, { color: themes[props.theme].buttonText }]} style={[styles.closeButton, { color: themes[props.theme].buttonText }]}
name='cross' name='Cross'
size={20} size={20}
/> />
<Text style={[styles.title, { color: themes[props.theme].buttonText }]}>{I18n.t('Reactions')}</Text> <Text style={[styles.title, { color: themes[props.theme].buttonText }]}>{I18n.t('Reactions')}</Text>

View File

@ -1,16 +1,13 @@
import React from 'react'; import React from 'react';
import { Image, StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { CustomIcon } from '../lib/Icons'; import { CustomIcon } from '../lib/Icons';
import { STATUS_COLORS, themes } from '../constants/colors'; import { STATUS_COLORS, themes } from '../constants/colors';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
style: { icon: {
marginRight: 7, marginTop: 3,
marginTop: 3 marginRight: 4
},
discussion: {
marginRight: 6
} }
}); });
@ -23,22 +20,32 @@ const RoomTypeIcon = React.memo(({
const color = themes[theme].auxiliaryText; const color = themes[theme].auxiliaryText;
let icon = 'lock';
if (type === 'discussion') { if (type === 'discussion') {
// FIXME: These are temporary only. We should have all room icons on <Customicon />, but our design team is still working on this. icon = 'chat';
return <CustomIcon name='chat' size={13} style={[styles.style, styles.iconColor, styles.discussion, { color }]} />; } else if (type === 'c') {
icon = 'hash';
} else if (type === 'd') {
if (isGroupChat) {
icon = 'team';
} else {
icon = 'at';
}
} else if (type === 'l') {
icon = 'livechat';
} }
if (type === 'c') { return (
return <Image source={{ uri: 'hashtag' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />; <CustomIcon
} if (type === 'd') { name={icon}
if (isGroupChat) { size={size}
return <CustomIcon name='team' size={13} style={[styles.style, styles.discussion, { color }]} />; style={[
} type === 'l' && status ? { color: STATUS_COLORS[status] } : { color },
return <CustomIcon name='at' size={13} style={[styles.style, styles.discussion, { color }]} />; styles.icon,
} if (type === 'l') { style
return <CustomIcon name='omnichannel' size={13} style={[styles.style, styles.discussion, { color: STATUS_COLORS[status] }]} />; ]}
} />
return <Image source={{ uri: 'lock' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />; );
}); });
RoomTypeIcon.propTypes = { RoomTypeIcon.propTypes = {
@ -51,7 +58,7 @@ RoomTypeIcon.propTypes = {
}; };
RoomTypeIcon.defaultProps = { RoomTypeIcon.defaultProps = {
size: 10 size: 16
}; };
export default RoomTypeIcon; export default RoomTypeIcon;

View File

@ -0,0 +1,34 @@
import React from 'react';
import { StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { SafeAreaView as SafeAreaContext } from 'react-native-safe-area-context';
import { themes } from '../constants/colors';
const styles = StyleSheet.create({
view: {
flex: 1
}
});
const SafeAreaView = React.memo(({
style, children, testID, theme, vertical = true, ...props
}) => (
<SafeAreaContext
style={[styles.view, { backgroundColor: themes[theme].auxiliaryBackground }, style]}
edges={vertical ? ['right', 'left'] : undefined}
testID={testID}
{...props}
>
{children}
</SafeAreaContext>
));
SafeAreaView.propTypes = {
testID: PropTypes.string,
theme: PropTypes.string,
vertical: PropTypes.bool,
style: PropTypes.object,
children: PropTypes.element
};
export default SafeAreaView;

View File

@ -4,18 +4,21 @@ import PropTypes from 'prop-types';
import { isIOS } from '../utils/deviceInfo'; import { isIOS } from '../utils/deviceInfo';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { withTheme } from '../theme';
const StatusBar = React.memo(({ theme }) => { const StatusBar = React.memo(({ theme, barStyle, backgroundColor }) => {
let barStyle = 'light-content'; if (!barStyle) {
if (theme === 'light' && isIOS) { barStyle = 'light-content';
barStyle = 'dark-content'; if (theme === 'light' && isIOS) {
barStyle = 'dark-content';
}
} }
return <StatusBarRN backgroundColor={themes[theme].headerBackground} barStyle={barStyle} animated />; return <StatusBarRN backgroundColor={backgroundColor ?? themes[theme].headerBackground} barStyle={barStyle} animated />;
}); });
StatusBar.propTypes = { StatusBar.propTypes = {
theme: PropTypes.string theme: PropTypes.string,
barStyle: PropTypes.string,
backgroundColor: PropTypes.string
}; };
export default withTheme(StatusBar); export default StatusBar;

View File

@ -111,7 +111,7 @@ export default class RCTextInput extends React.PureComponent {
return ( return (
<BorderlessButton onPress={this.tooglePassword} style={[styles.iconContainer, styles.iconRight]}> <BorderlessButton onPress={this.tooglePassword} style={[styles.iconContainer, styles.iconRight]}>
<CustomIcon <CustomIcon
name={showPassword ? 'Eye' : 'eye-off'} name={showPassword ? 'eye' : 'eye-off'}
testID={testID ? `${ testID }-icon-right` : null} testID={testID ? `${ testID }-icon-right` : null}
style={{ color: themes[theme].auxiliaryText }} style={{ color: themes[theme].auxiliaryText }}
size={20} size={20}

View File

@ -5,12 +5,12 @@ import PropTypes from 'prop-types';
import { sha256 } from 'js-sha256'; import { sha256 } from 'js-sha256';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
import useDeepCompareEffect from 'use-deep-compare-effect'; import useDeepCompareEffect from 'use-deep-compare-effect';
import { connect } from 'react-redux';
import TextInput from '../TextInput'; import TextInput from '../TextInput';
import I18n from '../../i18n'; import I18n from '../../i18n';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { withSplit } from '../../split';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import Button from '../Button'; import Button from '../Button';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
@ -36,7 +36,7 @@ const methods = {
} }
}; };
const TwoFactor = React.memo(({ theme, split }) => { const TwoFactor = React.memo(({ theme, isMasterDetail }) => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [data, setData] = useState({}); const [data, setData] = useState({});
const [code, setCode] = useState(''); const [code, setCode] = useState('');
@ -93,7 +93,7 @@ const TwoFactor = React.memo(({ theme, split }) => {
hideModalContentWhileAnimating hideModalContentWhileAnimating
> >
<View style={styles.container} testID='two-factor'> <View style={styles.container} testID='two-factor'>
<View style={[styles.content, split && [sharedStyles.modal, sharedStyles.modalFormSheet], { backgroundColor: themes[theme].backgroundColor }]}> <View style={[styles.content, isMasterDetail && [sharedStyles.modalFormSheet, styles.tablet], { backgroundColor: themes[theme].backgroundColor }]}>
<Text style={[styles.title, { color }]}>{I18n.t(method?.title || 'Two_Factor_Authentication')}</Text> <Text style={[styles.title, { color }]}>{I18n.t(method?.title || 'Two_Factor_Authentication')}</Text>
{method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null} {method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null}
<TextInput <TextInput
@ -134,7 +134,11 @@ const TwoFactor = React.memo(({ theme, split }) => {
}); });
TwoFactor.propTypes = { TwoFactor.propTypes = {
theme: PropTypes.string, theme: PropTypes.string,
split: PropTypes.bool isMasterDetail: PropTypes.bool
}; };
export default withSplit(withTheme(TwoFactor)); const mapStateToProps = state => ({
isMasterDetail: state.app.isMasterDetail
});
export default connect(mapStateToProps)(withTheme(TwoFactor));

View File

@ -37,5 +37,8 @@ export default StyleSheet.create({
buttonContainer: { buttonContainer: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between' justifyContent: 'space-between'
},
tablet: {
height: undefined
} }
}); });

View File

@ -24,7 +24,7 @@ const Chip = ({
<> <>
{item.imageUrl ? <FastImage style={styles.chipImage} source={{ uri: item.imageUrl }} /> : null} {item.imageUrl ? <FastImage style={styles.chipImage} source={{ uri: item.imageUrl }} /> : null}
<Text numberOfLines={1} style={[styles.chipText, { color: themes[theme].titleText }]}>{textParser([item.text])}</Text> <Text numberOfLines={1} style={[styles.chipText, { color: themes[theme].titleText }]}>{textParser([item.text])}</Text>
<CustomIcon name='cross' size={16} color={themes[theme].auxiliaryText} /> <CustomIcon name='Cross' size={16} color={themes[theme].auxiliaryText} />
</> </>
</Touchable> </Touchable>
); );

View File

@ -22,7 +22,7 @@ const Input = ({
{ {
loading loading
? <ActivityIndicator style={[styles.loading, styles.icon]} /> ? <ActivityIndicator style={[styles.loading, styles.icon]} />
: <CustomIcon name='arrow-down' size={22} color={themes[theme].auxiliaryText} style={styles.icon} /> : <CustomIcon name='chevron-down' size={22} color={themes[theme].auxiliaryText} style={styles.icon} />
} }
</View> </View>
</Touchable> </Touchable>

View File

@ -55,7 +55,7 @@ export const Select = ({
const Icon = () => ( const Icon = () => (
loading loading
? <ActivityIndicator style={styles.loading} /> ? <ActivityIndicator style={styles.loading} />
: <CustomIcon size={22} name='arrow-down' style={isAndroid && styles.icon} color={themes[theme].auxiliaryText} /> : <CustomIcon size={22} name='chevron-down' style={isAndroid && styles.icon} color={themes[theme].auxiliaryText} />
); );
return ( return (

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
View, StyleSheet, Text, Easing, Dimensions View, StyleSheet, Text, Easing
} from 'react-native'; } from 'react-native';
import { Audio } from 'expo-av'; import { Audio } from 'expo-av';
import Slider from '@react-native-community/slider'; import Slider from '@react-native-community/slider';
@ -15,9 +15,9 @@ import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { isAndroid, isIOS } from '../../utils/deviceInfo'; import { isAndroid, isIOS } from '../../utils/deviceInfo';
import { withSplit } from '../../split';
import MessageContext from './Context'; import MessageContext from './Context';
import ActivityIndicator from '../ActivityIndicator'; import ActivityIndicator from '../ActivityIndicator';
import { withDimensions } from '../../dimensions';
const mode = { const mode = {
allowsRecordingIOS: false, allowsRecordingIOS: false,
@ -98,8 +98,8 @@ class MessageAudio extends React.Component {
static propTypes = { static propTypes = {
file: PropTypes.object.isRequired, file: PropTypes.object.isRequired,
theme: PropTypes.string, theme: PropTypes.string,
split: PropTypes.bool, getCustomEmoji: PropTypes.func,
getCustomEmoji: PropTypes.func scale: PropTypes.number
} }
constructor(props) { constructor(props) {
@ -138,7 +138,7 @@ class MessageAudio extends React.Component {
const { const {
currentTime, duration, paused, loading currentTime, duration, paused, loading
} = this.state; } = this.state;
const { file, split, theme } = this.props; const { file, theme } = this.props;
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
return true; return true;
} }
@ -154,9 +154,6 @@ class MessageAudio extends React.Component {
if (!equal(nextProps.file, file)) { if (!equal(nextProps.file, file)) {
return true; return true;
} }
if (nextProps.split !== split) {
return true;
}
if (nextState.loading !== loading) { if (nextState.loading !== loading) {
return true; return true;
} }
@ -249,7 +246,7 @@ class MessageAudio extends React.Component {
loading, paused, currentTime, duration loading, paused, currentTime, duration
} = this.state; } = this.state;
const { const {
file, getCustomEmoji, split, theme file, getCustomEmoji, theme, scale
} = this.props; } = this.props;
const { description } = file; const { description } = file;
const { baseUrl, user } = this.context; const { baseUrl, user } = this.context;
@ -263,8 +260,7 @@ class MessageAudio extends React.Component {
<View <View
style={[ style={[
styles.audioContainer, styles.audioContainer,
{ backgroundColor: themes[theme].chatComponentBackground, borderColor: themes[theme].borderColor }, { backgroundColor: themes[theme].chatComponentBackground, borderColor: themes[theme].borderColor }
split && sharedStyles.tabletContent
]} ]}
> >
<Button loading={loading} paused={paused} onPress={this.togglePlayPause} theme={theme} /> <Button loading={loading} paused={paused} onPress={this.togglePlayPause} theme={theme} />
@ -279,7 +275,7 @@ class MessageAudio extends React.Component {
minimumTrackTintColor={themes[theme].tintColor} minimumTrackTintColor={themes[theme].tintColor}
maximumTrackTintColor={themes[theme].auxiliaryText} maximumTrackTintColor={themes[theme].auxiliaryText}
onValueChange={this.onValueChange} onValueChange={this.onValueChange}
thumbImage={isIOS && { uri: 'audio_thumb', scale: Dimensions.get('window').scale }} thumbImage={isIOS && { uri: 'audio_thumb', scale }}
/> />
<Text style={[styles.duration, { color: themes[theme].auxiliaryText }]}>{this.duration}</Text> <Text style={[styles.duration, { color: themes[theme].auxiliaryText }]}>{this.duration}</Text>
</View> </View>
@ -289,4 +285,4 @@ class MessageAudio extends React.Component {
} }
} }
export default withSplit(MessageAudio); export default withDimensions(MessageAudio);

View File

@ -22,7 +22,7 @@ const CallButton = React.memo(({
hitSlop={BUTTON_HIT_SLOP} hitSlop={BUTTON_HIT_SLOP}
> >
<> <>
<CustomIcon name='video' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} /> <CustomIcon name='video-1' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} />
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{I18n.t('Click_to_join')}</Text> <Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{I18n.t('Click_to_join')}</Text>
</> </>
</Touchable> </Touchable>

View File

@ -10,19 +10,17 @@ import Touchable from './Touchable';
import Markdown from '../markdown'; import Markdown from '../markdown';
import styles from './styles'; import styles from './styles';
import { formatAttachmentUrl } from '../../lib/utils'; import { formatAttachmentUrl } from '../../lib/utils';
import { withSplit } from '../../split';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles';
import MessageContext from './Context'; import MessageContext from './Context';
const ImageProgress = createImageProgress(FastImage); const ImageProgress = createImageProgress(FastImage);
const Button = React.memo(({ const Button = React.memo(({
children, onPress, split, theme children, onPress, theme
}) => ( }) => (
<Touchable <Touchable
onPress={onPress} onPress={onPress}
style={[styles.imageContainer, split && sharedStyles.tabletContent]} style={styles.imageContainer}
background={Touchable.Ripple(themes[theme].bannerBackground)} background={Touchable.Ripple(themes[theme].bannerBackground)}
> >
{children} {children}
@ -42,7 +40,7 @@ export const MessageImage = React.memo(({ img, theme }) => (
)); ));
const ImageContainer = React.memo(({ const ImageContainer = React.memo(({
file, imageUrl, showAttachment, getCustomEmoji, split, theme file, imageUrl, showAttachment, getCustomEmoji, theme
}) => { }) => {
const { baseUrl, user } = useContext(MessageContext); const { baseUrl, user } = useContext(MessageContext);
const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl); const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
@ -54,7 +52,7 @@ const ImageContainer = React.memo(({
if (file.description) { if (file.description) {
return ( return (
<Button split={split} theme={theme} onPress={onPress}> <Button theme={theme} onPress={onPress}>
<View> <View>
<MessageImage img={img} theme={theme} /> <MessageImage img={img} theme={theme} />
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} /> <Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
@ -64,19 +62,18 @@ const ImageContainer = React.memo(({
} }
return ( return (
<Button split={split} theme={theme} onPress={onPress}> <Button theme={theme} onPress={onPress}>
<MessageImage img={img} theme={theme} /> <MessageImage img={img} theme={theme} />
</Button> </Button>
); );
}, (prevProps, nextProps) => equal(prevProps.file, nextProps.file) && prevProps.split === nextProps.split && prevProps.theme === nextProps.theme); }, (prevProps, nextProps) => equal(prevProps.file, nextProps.file) && prevProps.theme === nextProps.theme);
ImageContainer.propTypes = { ImageContainer.propTypes = {
file: PropTypes.object, file: PropTypes.object,
imageUrl: PropTypes.string, imageUrl: PropTypes.string,
showAttachment: PropTypes.func, showAttachment: PropTypes.func,
theme: PropTypes.string, theme: PropTypes.string,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func
split: PropTypes.bool
}; };
ImageContainer.displayName = 'MessageImageContainer'; ImageContainer.displayName = 'MessageImageContainer';
@ -89,9 +86,8 @@ ImageContainer.displayName = 'MessageImage';
Button.propTypes = { Button.propTypes = {
children: PropTypes.node, children: PropTypes.node,
onPress: PropTypes.func, onPress: PropTypes.func,
theme: PropTypes.string, theme: PropTypes.string
split: PropTypes.bool
}; };
ImageContainer.displayName = 'MessageButton'; ImageContainer.displayName = 'MessageButton';
export default withSplit(ImageContainer); export default ImageContainer;

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