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
|
@ -53,13 +53,30 @@ save-gems-cache: &save-gems-cache
|
|||
paths:
|
||||
- vendor/bundle
|
||||
|
||||
update-fastlane: &update-fastlane
|
||||
update-fastlane-ios: &update-fastlane-ios
|
||||
name: Update Fastlane
|
||||
command: |
|
||||
echo "ruby-2.6.4" > ~/.ruby-version
|
||||
bundle install
|
||||
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
|
||||
name: Restore Brew cache
|
||||
key: brew-{{ checksum "yarn.lock" }}-{{ checksum ".circleci/config.yml" }}
|
||||
|
@ -227,9 +244,9 @@ jobs:
|
|||
|
||||
- run: *install-npm-modules
|
||||
|
||||
- restore_cache:
|
||||
name: Restore gradle cache
|
||||
key: android-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
|
||||
- run: *update-fastlane-android
|
||||
|
||||
- restore_cache: *restore-gradle-cache
|
||||
|
||||
- run:
|
||||
name: Configure Gradle
|
||||
|
@ -267,14 +284,10 @@ jobs:
|
|||
name: Build Android App
|
||||
command: |
|
||||
if [[ $KEYSTORE ]]; then
|
||||
./gradlew bundleRelease
|
||||
bundle exec fastlane android release
|
||||
else
|
||||
./gradlew assembleDebug
|
||||
bundle exec fastlane android build
|
||||
fi
|
||||
|
||||
mkdir -p /tmp/build
|
||||
|
||||
mv app/build/outputs /tmp/build/
|
||||
working_directory: android
|
||||
|
||||
- run:
|
||||
|
@ -291,15 +304,40 @@ jobs:
|
|||
fi
|
||||
|
||||
- store_artifacts:
|
||||
path: /tmp/build/outputs
|
||||
path: android/app/build/outputs
|
||||
|
||||
- save_cache: *save-npm-cache-linux
|
||||
|
||||
- save_cache:
|
||||
name: Save gradle cache
|
||||
key: android-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
|
||||
- save_cache: *save-gradle-cache
|
||||
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
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-build:
|
||||
|
@ -316,7 +354,7 @@ jobs:
|
|||
|
||||
- run: *install-npm-modules
|
||||
|
||||
- run: *update-fastlane
|
||||
- run: *update-fastlane-ios
|
||||
|
||||
- run:
|
||||
name: Set Google Services
|
||||
|
@ -378,7 +416,7 @@ jobs:
|
|||
|
||||
- restore_cache: *restore-gems-cache
|
||||
|
||||
- run: *update-fastlane
|
||||
- run: *update-fastlane-ios
|
||||
|
||||
- run:
|
||||
name: Fastlane Tesflight Upload
|
||||
|
@ -424,3 +462,10 @@ workflows:
|
|||
- android-build:
|
||||
requires:
|
||||
- lint-testunit
|
||||
- android-hold-google-play-alpha:
|
||||
type: approval
|
||||
requires:
|
||||
- android-build
|
||||
- android-google-play-alpha:
|
||||
requires:
|
||||
- android-hold-google-play-alpha
|
||||
|
|
|
@ -51,9 +51,10 @@ buck-out/
|
|||
# For more information about the recommended setup visit:
|
||||
# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
**/fastlane/report.xml
|
||||
**/fastlane/Preview.html
|
||||
**/fastlane/screenshots
|
||||
**/fastlane/test_output
|
||||
|
||||
coverage
|
||||
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
{}
|
||||
{
|
||||
"content_hash_max_items": 360000
|
||||
}
|
|
@ -69,7 +69,7 @@ Follow the [React Native Getting Started Guide](https://facebook.github.io/react
|
|||
|
||||
### Running single server
|
||||
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
|
||||
1) Omnichannel support
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -3,13 +3,8 @@
|
|||
package="chat.rocket.reactnative">
|
||||
|
||||
<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.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
|
||||
android:name=".MainApplication"
|
||||
|
@ -65,6 +60,7 @@
|
|||
android:theme="@style/AppTheme" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
|
|
|
@ -13,7 +13,8 @@ public class MainActivity extends ReactFragmentActivity {
|
|||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ public class BasePackageList {
|
|||
new expo.modules.keepawake.KeepAwakePackage(),
|
||||
new expo.modules.localauthentication.LocalAuthenticationPackage(),
|
||||
new expo.modules.permissions.PermissionsPackage(),
|
||||
new expo.modules.videothumbnails.VideoThumbnailsPackage(),
|
||||
new expo.modules.webbrowser.WebBrowserPackage()
|
||||
);
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 508 B |
Before Width: | Height: | Size: 132 B |
Before Width: | Height: | Size: 187 B |
Before Width: | Height: | Size: 488 B |
Before Width: | Height: | Size: 484 B |
Before Width: | Height: | Size: 942 B |
Before Width: | Height: | Size: 370 B |
Before Width: | Height: | Size: 114 B |
Before Width: | Height: | Size: 147 B |
Before Width: | Height: | Size: 759 B |
Before Width: | Height: | Size: 351 B |
Before Width: | Height: | Size: 597 B |
Before Width: | Height: | Size: 550 B |
Before Width: | Height: | Size: 138 B |
Before Width: | Height: | Size: 227 B |
Before Width: | Height: | Size: 820 B |
Before Width: | Height: | Size: 658 B |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 783 B |
Before Width: | Height: | Size: 157 B |
Before Width: | Height: | Size: 307 B |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 949 B |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 920 B |
Before Width: | Height: | Size: 191 B |
Before Width: | Height: | Size: 376 B |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.6 KiB |
|
@ -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")
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
# Autogenerated by fastlane
|
||||
#
|
||||
# Ensure this file is checked in to source control!
|
||||
|
||||
|
|
@ -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).
|
|
@ -26,7 +26,7 @@ android.useAndroidX=true
|
|||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
APPLICATIONID=chat.rocket.reactnative
|
||||
VERSIONNAME=4.5.1
|
||||
VERSIONNAME=4.8.0
|
||||
VERSIONCODE=1
|
||||
BugsnagAPIKey=""
|
||||
KEYSTORE=my-upload-key.keystore
|
||||
|
|
|
@ -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;
|
|
@ -12,7 +12,8 @@ function createRequestTypes(base, types = defaultTypes) {
|
|||
export const LOGIN = createRequestTypes('LOGIN', [
|
||||
...defaultTypes,
|
||||
'SET_SERVICES',
|
||||
'SET_PREFERENCE'
|
||||
'SET_PREFERENCE',
|
||||
'SET_LOCAL_AUTHENTICATED'
|
||||
]);
|
||||
export const SHARE = createRequestTypes('SHARE', [
|
||||
'SELECT_SERVER',
|
||||
|
@ -32,7 +33,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
|
|||
'CLOSE_SEARCH_HEADER'
|
||||
]);
|
||||
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 CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...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 DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
|
||||
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 SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
|
||||
export const SET_ACTIVE_USERS = 'SET_ACTIVE_USERS';
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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'
|
||||
};
|
||||
}
|
|
@ -49,3 +49,10 @@ export function setPreference(preference) {
|
|||
preference
|
||||
};
|
||||
}
|
||||
|
||||
export function setLocalAuthenticated(isLocalAuthenticated) {
|
||||
return {
|
||||
type: types.LOGIN.SET_LOCAL_AUTHENTICATED,
|
||||
isLocalAuthenticated
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -44,9 +44,10 @@ export function serverFailure(err) {
|
|||
};
|
||||
}
|
||||
|
||||
export function serverInitAdd() {
|
||||
export function serverInitAdd(previousServer) {
|
||||
return {
|
||||
type: SERVER.INIT_ADD
|
||||
type: SERVER.INIT_ADD,
|
||||
previousServer
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable no-bitwise */
|
||||
import { constants } from 'react-native-keycommands';
|
||||
import KeyCommands, { constants } from 'react-native-keycommands';
|
||||
|
||||
import I18n from './i18n';
|
||||
|
||||
|
@ -17,7 +17,7 @@ const KEY_ADD_SERVER = __DEV__ ? 'l' : 'n';
|
|||
const KEY_SEND_MESSAGE = '\r';
|
||||
const KEY_SELECT = '123456789';
|
||||
|
||||
export const defaultCommands = [
|
||||
const keyCommands = [
|
||||
{
|
||||
// Focus messageBox
|
||||
input: KEY_TYPING,
|
||||
|
@ -29,10 +29,7 @@ export const defaultCommands = [
|
|||
input: KEY_SEND_MESSAGE,
|
||||
modifierFlags: 0,
|
||||
discoverabilityTitle: I18n.t('Send')
|
||||
}
|
||||
];
|
||||
|
||||
export const keyCommands = [
|
||||
},
|
||||
{
|
||||
// Open Preferences Modal
|
||||
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 commandHandle = (event, key, flags = []) => {
|
||||
|
|
|
@ -53,7 +53,9 @@ export const themes = {
|
|||
passcodePrimary: '#2F343D',
|
||||
passcodeSecondary: '#6C727A',
|
||||
passcodeDotEmpty: '#CBCED1',
|
||||
passcodeDotFull: '#6C727A'
|
||||
passcodeDotFull: '#6C727A',
|
||||
previewBackground: '#1F2329',
|
||||
previewTintColor: '#ffffff'
|
||||
},
|
||||
dark: {
|
||||
backgroundColor: '#030b1b',
|
||||
|
@ -95,7 +97,9 @@ export const themes = {
|
|||
passcodePrimary: '#FFFFFF',
|
||||
passcodeSecondary: '#CBCED1',
|
||||
passcodeDotEmpty: '#CBCED1',
|
||||
passcodeDotFull: '#6C727A'
|
||||
passcodeDotFull: '#6C727A',
|
||||
previewBackground: '#030b1b',
|
||||
previewTintColor: '#ffffff'
|
||||
},
|
||||
black: {
|
||||
backgroundColor: '#000000',
|
||||
|
@ -137,6 +141,8 @@ export const themes = {
|
|||
passcodePrimary: '#FFFFFF',
|
||||
passcodeSecondary: '#CBCED1',
|
||||
passcodeDotEmpty: '#CBCED1',
|
||||
passcodeDotFull: '#6C727A'
|
||||
passcodeDotFull: '#6C727A',
|
||||
previewBackground: '#000000',
|
||||
previewTintColor: '#ffffff'
|
||||
}
|
||||
};
|
||||
|
|
|
@ -50,6 +50,18 @@ export default {
|
|||
Accounts_ManuallyApproveNewUsers: {
|
||||
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: {
|
||||
type: 'valueAsBoolean'
|
||||
},
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
export const MAX_SIDEBAR_WIDTH = 321;
|
||||
export const MAX_CONTENT_WIDTH = '90%';
|
||||
export const MAX_SCREEN_CONTENT_WIDTH = '50%';
|
||||
export const MIN_WIDTH_SPLIT_LAYOUT = 700;
|
||||
export const MIN_WIDTH_MASTER_DETAIL_LAYOUT = 700;
|
||||
|
|
|
@ -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';
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export * from './Provider';
|
||||
export * from './Button';
|
|
@ -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
|
||||
}
|
||||
});
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import { View, Image, StyleSheet } from 'react-native';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { themes } from '../constants/colors';
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
disclosureContainer: {
|
||||
|
@ -10,17 +11,14 @@ const styles = StyleSheet.create({
|
|||
marginRight: 9,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
disclosureIndicator: {
|
||||
width: 20,
|
||||
height: 20
|
||||
}
|
||||
});
|
||||
|
||||
export const DisclosureImage = React.memo(({ theme }) => (
|
||||
<Image
|
||||
source={{ uri: 'disclosure_indicator' }}
|
||||
style={[styles.disclosureIndicator, { tintColor: themes[theme].auxiliaryTintColor }]}
|
||||
<CustomIcon
|
||||
name='chevron-right'
|
||||
color={themes[theme].auxiliaryTintColor}
|
||||
size={20}
|
||||
/>
|
||||
));
|
||||
DisclosureImage.propTypes = {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Text, TouchableOpacity, FlatList } from 'react-native';
|
||||
import { responsive } from 'react-native-responsive-ui';
|
||||
|
||||
import shortnameToUnicode from '../../utils/shortnameToUnicode';
|
||||
import styles from './styles';
|
||||
|
@ -25,7 +24,6 @@ class EmojiCategory extends React.Component {
|
|||
static propTypes = {
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
emojis: PropTypes.any,
|
||||
window: PropTypes.any,
|
||||
onEmojiSelected: PropTypes.func,
|
||||
emojisPerRow: PropTypes.number,
|
||||
width: PropTypes.number
|
||||
|
@ -73,4 +71,4 @@ class EmojiCategory extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default responsive(EmojiCategory);
|
||||
export default EmojiCategory;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import { ScrollView, StyleSheet, View } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SafeAreaView } from 'react-navigation';
|
||||
|
||||
import { themes } from '../constants/colors';
|
||||
import sharedStyles from '../views/Styles';
|
||||
|
@ -10,6 +9,7 @@ import KeyboardView from '../presentation/KeyboardView';
|
|||
import StatusBar from './StatusBar';
|
||||
import AppVersion from './AppVersion';
|
||||
import { isTablet } from '../utils/deviceInfo';
|
||||
import SafeAreaView from './SafeAreaView';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scrollView: {
|
||||
|
@ -31,7 +31,7 @@ const FormContainer = ({ children, theme, testID }) => (
|
|||
>
|
||||
<StatusBar theme={theme} />
|
||||
<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}
|
||||
<AppVersion theme={theme} />
|
||||
</SafeAreaView>
|
||||
|
|
|
@ -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;
|
|
@ -36,9 +36,11 @@ export const DrawerButton = React.memo(({ navigation, testID, ...otherProps }) =
|
|||
</CustomHeaderButtons>
|
||||
));
|
||||
|
||||
export const CloseModalButton = React.memo(({ navigation, testID, onPress = () => navigation.pop() }) => (
|
||||
export const CloseModalButton = React.memo(({
|
||||
navigation, testID, onPress = () => navigation.pop(), ...props
|
||||
}) => (
|
||||
<CustomHeaderButtons left>
|
||||
<Item title='close' iconName='cross' onPress={onPress} testID={testID} />
|
||||
<Item title='close' iconName='Cross' onPress={onPress} testID={testID} {...props} />
|
||||
</CustomHeaderButtons>
|
||||
));
|
||||
|
||||
|
@ -46,7 +48,7 @@ export const CancelModalButton = React.memo(({ onPress, testID }) => (
|
|||
<CustomHeaderButtons left>
|
||||
{isIOS
|
||||
? <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>
|
||||
));
|
||||
|
@ -57,9 +59,9 @@ export const MoreButton = React.memo(({ onPress, testID }) => (
|
|||
</CustomHeaderButtons>
|
||||
));
|
||||
|
||||
export const SaveButton = React.memo(({ onPress, testID }) => (
|
||||
export const SaveButton = React.memo(({ onPress, testID, ...props }) => (
|
||||
<CustomHeaderButtons>
|
||||
<Item title='save' iconName='Download' onPress={onPress} testID={testID} />
|
||||
<Item title='save' iconName='download' onPress={onPress} testID={testID} {...props} />
|
||||
</CustomHeaderButtons>
|
||||
));
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
|
@ -5,7 +5,6 @@ import {
|
|||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { Base64 } from 'js-base64';
|
||||
import { withNavigation } from 'react-navigation';
|
||||
|
||||
import { withTheme } from '../theme';
|
||||
import sharedStyles from '../views/Styles';
|
||||
|
@ -361,4 +360,4 @@ const mapDispatchToProps = dispatch => ({
|
|||
loginRequest: params => dispatch(loginRequestAction(params))
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(withNavigation(LoginServices)));
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(LoginServices));
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -32,7 +32,7 @@ const Item = ({ item, theme }) => {
|
|||
{ loading ? <ActivityIndicator theme={theme} /> : null }
|
||||
</FastImage>
|
||||
)
|
||||
: <CustomIcon name='file-generic' size={36} color={themes[theme].actionTintColor} />
|
||||
: <CustomIcon name='clip' size={36} color={themes[theme].actionTintColor} />
|
||||
}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
|
|
@ -1,22 +1,28 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View } from 'react-native';
|
||||
|
||||
import { CancelEditingButton, ActionsButton } from './buttons';
|
||||
import styles from './styles';
|
||||
|
||||
const LeftButtons = React.memo(({
|
||||
theme, showMessageBoxActions, editing, editCancel
|
||||
theme, showMessageBoxActions, editing, editCancel, isActionsEnabled
|
||||
}) => {
|
||||
if (editing) {
|
||||
return <CancelEditingButton onPress={editCancel} theme={theme} />;
|
||||
}
|
||||
if (isActionsEnabled) {
|
||||
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
|
||||
}
|
||||
return <View style={styles.buttonsWhitespace} />;
|
||||
});
|
||||
|
||||
LeftButtons.propTypes = {
|
||||
theme: PropTypes.string,
|
||||
showMessageBoxActions: PropTypes.func.isRequired,
|
||||
editing: PropTypes.bool,
|
||||
editCancel: PropTypes.func.isRequired
|
||||
editCancel: PropTypes.func.isRequired,
|
||||
isActionsEnabled: PropTypes.bool
|
||||
};
|
||||
|
||||
export default LeftButtons;
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
View, SafeAreaView, PermissionsAndroid, Text
|
||||
View, PermissionsAndroid, Text
|
||||
} from 'react-native';
|
||||
import { AudioRecorder, AudioUtils } from 'react-native-audio';
|
||||
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||
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 I18n from '../../i18n';
|
||||
import { isIOS, isAndroid } from '../../utils/deviceInfo';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { themes } from '../../constants/colors';
|
||||
import SafeAreaView from '../SafeAreaView';
|
||||
|
||||
export const _formatTime = function(seconds) {
|
||||
let minutes = Math.floor(seconds / 60);
|
||||
|
@ -93,9 +94,6 @@ export default class extends React.PureComponent {
|
|||
if (!didSucceed) {
|
||||
return onFinish && onFinish(didSucceed);
|
||||
}
|
||||
if (isAndroid) {
|
||||
filePath = filePath.startsWith('file://') ? filePath : `file://${ filePath }`;
|
||||
}
|
||||
const fileInfo = {
|
||||
name: this.name,
|
||||
mime: 'audio/aac',
|
||||
|
@ -110,9 +108,10 @@ export default class extends React.PureComponent {
|
|||
finishAudioMessage = async() => {
|
||||
try {
|
||||
this.recording = false;
|
||||
const filePath = await AudioRecorder.stopRecording();
|
||||
let filePath = await AudioRecorder.stopRecording();
|
||||
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);
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -134,6 +133,7 @@ export default class extends React.PureComponent {
|
|||
return (
|
||||
<SafeAreaView
|
||||
testID='messagebox-recording'
|
||||
theme={theme}
|
||||
style={[
|
||||
styles.textBox,
|
||||
{ borderTopColor: themes[theme].borderColor }
|
||||
|
@ -149,7 +149,7 @@ export default class extends React.PureComponent {
|
|||
<CustomIcon
|
||||
size={22}
|
||||
color={themes[theme].dangerColor}
|
||||
name='cross'
|
||||
name='Cross'
|
||||
/>
|
||||
</BorderlessButton>
|
||||
<Text key='currentTime' style={[styles.textBoxInput, { color: themes[theme].titleText }]}>{currentTime}</Text>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { View, Text, StyleSheet } from 'react-native';
|
|||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import { connect } from 'react-redux';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
import Markdown from '../markdown';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
|
@ -58,7 +59,7 @@ const ReplyPreview = React.memo(({
|
|||
>
|
||||
<View style={[styles.messageContainer, { backgroundColor: themes[theme].chatComponentBackground }]}>
|
||||
<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>
|
||||
</View>
|
||||
<Markdown
|
||||
|
@ -71,10 +72,10 @@ const ReplyPreview = React.memo(({
|
|||
theme={theme}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
}, (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 = {
|
||||
replying: PropTypes.bool,
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View } from 'react-native';
|
||||
|
||||
import { SendButton, AudioButton, ActionsButton } from './buttons';
|
||||
import styles from './styles';
|
||||
|
||||
const RightButtons = React.memo(({
|
||||
theme, showSend, submit, recordAudioMessage, recordAudioMessageEnabled, showMessageBoxActions
|
||||
theme, showSend, submit, recordAudioMessage, recordAudioMessageEnabled, showMessageBoxActions, isActionsEnabled
|
||||
}) => {
|
||||
if (showSend) {
|
||||
return <SendButton onPress={submit} theme={theme} />;
|
||||
}
|
||||
if (recordAudioMessageEnabled) {
|
||||
if (recordAudioMessageEnabled || isActionsEnabled) {
|
||||
return (
|
||||
<>
|
||||
<AudioButton onPress={recordAudioMessage} theme={theme} />
|
||||
<ActionsButton onPress={showMessageBoxActions} theme={theme} />
|
||||
{recordAudioMessageEnabled ? <AudioButton onPress={recordAudioMessage} theme={theme} /> : null}
|
||||
{isActionsEnabled ? <ActionsButton onPress={showMessageBoxActions} theme={theme} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
|
||||
return <View style={styles.buttonsWhitespace} />;
|
||||
});
|
||||
|
||||
RightButtons.propTypes = {
|
||||
|
@ -26,7 +28,8 @@ RightButtons.propTypes = {
|
|||
submit: PropTypes.func.isRequired,
|
||||
recordAudioMessage: PropTypes.func.isRequired,
|
||||
recordAudioMessageEnabled: PropTypes.bool,
|
||||
showMessageBoxActions: PropTypes.func.isRequired
|
||||
showMessageBoxActions: PropTypes.func.isRequired,
|
||||
isActionsEnabled: PropTypes.bool
|
||||
};
|
||||
|
||||
export default RightButtons;
|
||||
|
|
|
@ -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)));
|
|
@ -17,7 +17,7 @@ const BaseButton = React.memo(({
|
|||
accessibilityLabel={I18n.t(accessibilityLabel)}
|
||||
accessibilityTraits='button'
|
||||
>
|
||||
<CustomIcon name={icon} size={23} color={themes[theme].tintColor} />
|
||||
<CustomIcon name={icon} size={25} color={themes[theme].tintColor} />
|
||||
</BorderlessButton>
|
||||
));
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ const CancelEditingButton = React.memo(({ theme, onPress }) => (
|
|||
onPress={onPress}
|
||||
testID='messagebox-cancel-editing'
|
||||
accessibilityLabel='Cancel_editing'
|
||||
icon='cross'
|
||||
icon='Cross'
|
||||
theme={theme}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -8,7 +8,7 @@ const SendButton = React.memo(({ theme, onPress }) => (
|
|||
onPress={onPress}
|
||||
testID='messagebox-send-message'
|
||||
accessibilityLabel='Send_message'
|
||||
icon='Send-active'
|
||||
icon='send-active'
|
||||
theme={theme}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import React, { Component } from 'react';
|
||||
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 { KeyboardAccessoryView } from 'react-native-keyboard-input';
|
||||
import ImagePicker from 'react-native-image-crop-picker';
|
||||
import equal from 'deep-equal';
|
||||
import DocumentPicker from 'react-native-document-picker';
|
||||
import ActionSheet from 'react-native-action-sheet';
|
||||
import { Q } from '@nozbe/watermelondb';
|
||||
|
||||
import { generateTriggerId } from '../../lib/methods/actions';
|
||||
|
@ -17,7 +18,6 @@ import styles from './styles';
|
|||
import database from '../../lib/database';
|
||||
import { emojis } from '../../emojis';
|
||||
import Recording from './Recording';
|
||||
import UploadModal from './UploadModal';
|
||||
import log from '../../utils/log';
|
||||
import I18n from '../../i18n';
|
||||
import ReplyPreview from './ReplyPreview';
|
||||
|
@ -43,9 +43,9 @@ import {
|
|||
MENTIONS_TRACKING_TYPE_USERS
|
||||
} from './constants';
|
||||
import CommandsPreview from './CommandsPreview';
|
||||
import { Review } from '../../utils/review';
|
||||
import { getUserSelector } from '../../selectors/login';
|
||||
import Navigation from '../../lib/Navigation';
|
||||
import { withActionSheet } from '../ActionSheet';
|
||||
|
||||
const imagePickerConfig = {
|
||||
cropping: true,
|
||||
|
@ -54,6 +54,7 @@ const imagePickerConfig = {
|
|||
};
|
||||
|
||||
const libraryPickerConfig = {
|
||||
multiple: true,
|
||||
mediaType: 'any'
|
||||
};
|
||||
|
||||
|
@ -61,13 +62,6 @@ const videoPickerConfig = {
|
|||
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 {
|
||||
static propTypes = {
|
||||
rid: PropTypes.string.isRequired,
|
||||
|
@ -95,7 +89,24 @@ class MessageBox extends Component {
|
|||
typing: PropTypes.func,
|
||||
theme: PropTypes.string,
|
||||
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) {
|
||||
|
@ -103,26 +114,45 @@ class MessageBox extends Component {
|
|||
this.state = {
|
||||
mentions: [],
|
||||
showEmojiKeyboard: false,
|
||||
showSend: false,
|
||||
showSend: props.showSend,
|
||||
recording: false,
|
||||
trackingType: '',
|
||||
file: {
|
||||
isVisible: false
|
||||
},
|
||||
commandPreview: [],
|
||||
showCommandPreview: false,
|
||||
command: {}
|
||||
};
|
||||
this.text = '';
|
||||
this.focused = false;
|
||||
this.messageBoxActions = [
|
||||
I18n.t('Cancel'),
|
||||
I18n.t('Take_a_photo'),
|
||||
I18n.t('Take_a_video'),
|
||||
I18n.t('Choose_from_library'),
|
||||
I18n.t('Choose_file'),
|
||||
I18n.t('Create_Discussion')
|
||||
|
||||
// MessageBox Actions
|
||||
this.options = [
|
||||
{
|
||||
title: I18n.t('Take_a_photo'),
|
||||
icon: 'image',
|
||||
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 = {
|
||||
cropperChooseText: I18n.t('Choose'),
|
||||
cropperCancelText: I18n.t('Cancel'),
|
||||
|
@ -144,27 +174,29 @@ class MessageBox extends Component {
|
|||
|
||||
async componentDidMount() {
|
||||
const db = database.active;
|
||||
const { rid, tmid, navigation } = this.props;
|
||||
const {
|
||||
rid, tmid, navigation, sharing
|
||||
} = this.props;
|
||||
let msg;
|
||||
try {
|
||||
const threadsCollection = db.collections.get('threads');
|
||||
const subsCollection = db.collections.get('subscriptions');
|
||||
try {
|
||||
this.room = await subsCollection.find(rid);
|
||||
} catch (error) {
|
||||
console.log('Messagebox.didMount: Room not found');
|
||||
}
|
||||
if (tmid) {
|
||||
try {
|
||||
const thread = await threadsCollection.find(tmid);
|
||||
if (thread) {
|
||||
msg = thread.draftMessage;
|
||||
this.thread = await threadsCollection.find(tmid);
|
||||
if (this.thread && !sharing) {
|
||||
msg = this.thread.draftMessage;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Messagebox.didMount: Thread not found');
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
this.room = await subsCollection.find(rid);
|
||||
msg = this.room.draftMessage;
|
||||
} catch (error) {
|
||||
console.log('Messagebox.didMount: Room not found');
|
||||
}
|
||||
} else if (!sharing) {
|
||||
msg = this.room?.draftMessage;
|
||||
}
|
||||
} catch (e) {
|
||||
log(e);
|
||||
|
@ -183,16 +215,25 @@ class MessageBox extends Component {
|
|||
EventEmiter.addEventListener(KEY_COMMAND, this.handleCommands);
|
||||
}
|
||||
|
||||
this.didFocusListener = navigation.addListener('didFocus', () => {
|
||||
this.unsubscribeFocus = navigation.addListener('focus', () => {
|
||||
if (this.tracking && this.tracking.resetTracking) {
|
||||
this.tracking.resetTracking();
|
||||
}
|
||||
});
|
||||
this.unsubscribeBlur = navigation.addListener('blur', () => {
|
||||
this.component?.blur();
|
||||
});
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
const { isFocused, editing, replying } = this.props;
|
||||
if (!isFocused()) {
|
||||
const {
|
||||
isFocused, editing, replying, sharing
|
||||
} = this.props;
|
||||
if (!isFocused?.()) {
|
||||
return;
|
||||
}
|
||||
if (sharing) {
|
||||
this.setInput(nextProps.message.msg ?? '');
|
||||
return;
|
||||
}
|
||||
if (editing !== nextProps.editing && nextProps.editing) {
|
||||
|
@ -200,6 +241,7 @@ class MessageBox extends Component {
|
|||
if (this.text) {
|
||||
this.setShowSend(true);
|
||||
}
|
||||
this.focus();
|
||||
} else if (replying !== nextProps.replying && nextProps.replying) {
|
||||
this.focus();
|
||||
} else if (!nextProps.message) {
|
||||
|
@ -209,11 +251,11 @@ class MessageBox extends Component {
|
|||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const {
|
||||
showEmojiKeyboard, showSend, recording, mentions, file, commandPreview
|
||||
showEmojiKeyboard, showSend, recording, mentions, commandPreview
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
roomType, replying, editing, isFocused, theme
|
||||
roomType, replying, editing, isFocused, message, theme, children
|
||||
} = this.props;
|
||||
if (nextProps.theme !== theme) {
|
||||
return true;
|
||||
|
@ -245,7 +287,10 @@ class MessageBox extends Component {
|
|||
if (!equal(nextState.commandPreview, commandPreview)) {
|
||||
return true;
|
||||
}
|
||||
if (!equal(nextState.file, file)) {
|
||||
if (!equal(nextProps.message, message)) {
|
||||
return true;
|
||||
}
|
||||
if (!equal(nextProps.children, children)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -268,8 +313,11 @@ class MessageBox extends Component {
|
|||
if (this.getSlashCommands && this.getSlashCommands.stop) {
|
||||
this.getSlashCommands.stop();
|
||||
}
|
||||
if (this.didFocusListener && this.didFocusListener.remove) {
|
||||
this.didFocusListener.remove();
|
||||
if (this.unsubscribeFocus) {
|
||||
this.unsubscribeFocus();
|
||||
}
|
||||
if (this.unsubscribeBlur) {
|
||||
this.unsubscribeBlur();
|
||||
}
|
||||
if (isTablet) {
|
||||
EventEmiter.removeListener(KEY_COMMAND, this.handleCommands);
|
||||
|
@ -285,10 +333,13 @@ class MessageBox extends Component {
|
|||
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
debouncedOnChangeText = debounce(async(text) => {
|
||||
const { sharing } = this.props;
|
||||
const db = database.active;
|
||||
const isTextEmpty = text.length === 0;
|
||||
// this.setShowSend(!isTextEmpty);
|
||||
this.handleTyping(!isTextEmpty);
|
||||
|
||||
if (!sharing) {
|
||||
// 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 (slashCommand) {
|
||||
|
@ -303,6 +354,7 @@ class MessageBox extends Component {
|
|||
console.log('Slash command not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isTextEmpty) {
|
||||
try {
|
||||
|
@ -310,13 +362,21 @@ class MessageBox extends Component {
|
|||
const cursor = Math.max(start, end);
|
||||
const lastNativeText = this.component?.lastNativeText || '';
|
||||
// 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);
|
||||
if (!result) {
|
||||
if (!sharing) {
|
||||
const slash = lastNativeText.match(/^\/$/); // matches only '/' in input
|
||||
if (slash) {
|
||||
return this.identifyMentionKeyword('', MENTIONS_TRACKING_TYPE_COMMANDS);
|
||||
}
|
||||
}
|
||||
return this.stopTrackingMention();
|
||||
}
|
||||
const [, lastChar, name] = result;
|
||||
|
@ -455,7 +515,10 @@ class MessageBox extends Component {
|
|||
}
|
||||
|
||||
handleTyping = (isTyping) => {
|
||||
const { typing, rid } = this.props;
|
||||
const { typing, rid, sharing } = this.props;
|
||||
if (sharing) {
|
||||
return;
|
||||
}
|
||||
if (!isTyping) {
|
||||
if (this.typingTimeout) {
|
||||
clearTimeout(this.typingTimeout);
|
||||
|
@ -495,7 +558,8 @@ class MessageBox extends Component {
|
|||
|
||||
setShowSend = (showSend) => {
|
||||
const { showSend: prevShowSend } = this.state;
|
||||
if (prevShowSend !== showSend) {
|
||||
const { showSend: propShowSend } = this.props;
|
||||
if (prevShowSend !== showSend && !propShowSend) {
|
||||
this.setState({ showSend });
|
||||
}
|
||||
}
|
||||
|
@ -507,7 +571,7 @@ class MessageBox extends Component {
|
|||
|
||||
canUploadFile = (file) => {
|
||||
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) {
|
||||
return true;
|
||||
}
|
||||
|
@ -515,33 +579,11 @@ class MessageBox extends Component {
|
|||
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() => {
|
||||
try {
|
||||
const image = await ImagePicker.openCamera(this.imagePickerConfig);
|
||||
if (this.canUploadFile(image)) {
|
||||
this.showUploadModal(image);
|
||||
this.openShareView([image]);
|
||||
}
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
|
@ -552,7 +594,7 @@ class MessageBox extends Component {
|
|||
try {
|
||||
const video = await ImagePicker.openCamera(this.videoPickerConfig);
|
||||
if (this.canUploadFile(video)) {
|
||||
this.showUploadModal(video);
|
||||
this.openShareView([video]);
|
||||
}
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
|
@ -561,10 +603,8 @@ class MessageBox extends Component {
|
|||
|
||||
chooseFromLibrary = async() => {
|
||||
try {
|
||||
const image = await ImagePicker.openPicker(this.libraryPickerConfig);
|
||||
if (this.canUploadFile(image)) {
|
||||
this.showUploadModal(image);
|
||||
}
|
||||
const attachments = await ImagePicker.openPicker(this.libraryPickerConfig);
|
||||
this.openShareView(attachments);
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
|
@ -582,7 +622,7 @@ class MessageBox extends Component {
|
|||
path: res.uri
|
||||
};
|
||||
if (this.canUploadFile(file)) {
|
||||
this.showUploadModal(file);
|
||||
this.openShareView([file]);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!DocumentPicker.isCancel(e)) {
|
||||
|
@ -591,43 +631,30 @@ class MessageBox extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
createDiscussion = () => {
|
||||
Navigation.navigate('CreateDiscussionView', { channel: this.room });
|
||||
openShareView = (attachments) => {
|
||||
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) => {
|
||||
this.setState({ file: { ...file, isVisible: true } });
|
||||
createDiscussion = () => {
|
||||
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 = () => {
|
||||
ActionSheet.showActionSheetWithOptions({
|
||||
options: this.messageBoxActions,
|
||||
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;
|
||||
}
|
||||
const { showActionSheet } = this.props;
|
||||
showActionSheet({ options: this.options });
|
||||
}
|
||||
|
||||
editCancel = () => {
|
||||
|
@ -672,16 +699,22 @@ class MessageBox extends Component {
|
|||
|
||||
submit = async() => {
|
||||
const {
|
||||
onSubmit, rid: roomId, tmid
|
||||
onSubmit, rid: roomId, tmid, showSend, sharing
|
||||
} = this.props;
|
||||
const message = this.text;
|
||||
|
||||
// if sharing, only execute onSubmit prop
|
||||
if (sharing) {
|
||||
onSubmit(message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearInput();
|
||||
this.debouncedOnChangeText.stop();
|
||||
this.closeEmoji();
|
||||
this.stopTrackingMention();
|
||||
this.handleTyping(false);
|
||||
if (message.trim() === '') {
|
||||
if (message.trim() === '' && !showSend) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -802,7 +835,7 @@ class MessageBox extends Component {
|
|||
recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview
|
||||
} = this.state;
|
||||
const {
|
||||
editing, message, replying, replyCancel, user, getCustomEmoji, theme, Message_AudioRecorderEnabled
|
||||
editing, message, replying, replyCancel, user, getCustomEmoji, theme, Message_AudioRecorderEnabled, children, isActionsEnabled
|
||||
} = this.props;
|
||||
|
||||
const isAndroidTablet = isTablet && isAndroid ? {
|
||||
|
@ -839,6 +872,7 @@ class MessageBox extends Component {
|
|||
showEmojiKeyboard={showEmojiKeyboard}
|
||||
editing={editing}
|
||||
showMessageBoxActions={this.showMessageBoxActions}
|
||||
isActionsEnabled={isActionsEnabled}
|
||||
editCancel={this.editCancel}
|
||||
openEmoji={this.openEmoji}
|
||||
closeEmoji={this.closeEmoji}
|
||||
|
@ -865,17 +899,21 @@ class MessageBox extends Component {
|
|||
recordAudioMessage={this.recordAudioMessage}
|
||||
recordAudioMessageEnabled={Message_AudioRecorderEnabled}
|
||||
showMessageBoxActions={this.showMessageBoxActions}
|
||||
isActionsEnabled={isActionsEnabled}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
console.count(`${ this.constructor.name }.render calls`);
|
||||
const { showEmojiKeyboard, file } = this.state;
|
||||
const { user, baseUrl, theme } = this.props;
|
||||
const { showEmojiKeyboard } = this.state;
|
||||
const {
|
||||
user, baseUrl, theme, iOSScrollBehavior
|
||||
} = this.props;
|
||||
return (
|
||||
<MessageboxContext.Provider
|
||||
value={{
|
||||
|
@ -897,12 +935,7 @@ class MessageBox extends Component {
|
|||
requiresSameParentToManageScrollView
|
||||
addBottomView
|
||||
bottomViewColor={themes[theme].messageboxBackground}
|
||||
/>
|
||||
<UploadModal
|
||||
isVisible={(file && file.isVisible)}
|
||||
file={file}
|
||||
close={() => this.setState({ file: {} })}
|
||||
submit={this.sendMediaMessage}
|
||||
iOSScrollBehavior={iOSScrollBehavior}
|
||||
/>
|
||||
</MessageboxContext.Provider>
|
||||
);
|
||||
|
@ -910,6 +943,7 @@ class MessageBox extends Component {
|
|||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
isMasterDetail: state.app.isMasterDetail,
|
||||
baseUrl: state.server.server,
|
||||
threadsEnabled: state.settings.Threads_enabled,
|
||||
user: getUserSelector(state),
|
||||
|
@ -922,4 +956,4 @@ const dispatchToProps = ({
|
|||
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));
|
||||
|
|
|
@ -103,5 +103,8 @@ export default StyleSheet.create({
|
|||
},
|
||||
scrollViewMention: {
|
||||
maxHeight: SCROLLVIEW_MENTION_HEIGHT
|
||||
},
|
||||
buttonsWhitespace: {
|
||||
width: 15
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,41 +1,22 @@
|
|||
import React from 'react';
|
||||
import { useImperativeHandle, forwardRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ActionSheet from 'react-native-action-sheet';
|
||||
|
||||
import RocketChat from '../lib/rocketchat';
|
||||
import database from '../lib/database';
|
||||
import protectedFunction from '../lib/methods/helpers/protectedFunction';
|
||||
import { useActionSheet } from './ActionSheet';
|
||||
import I18n from '../i18n';
|
||||
import log from '../utils/log';
|
||||
|
||||
class MessageErrorActions extends React.Component {
|
||||
static propTypes = {
|
||||
actionsHide: PropTypes.func.isRequired,
|
||||
message: PropTypes.object,
|
||||
tmid: PropTypes.string
|
||||
};
|
||||
const MessageErrorActions = forwardRef(({ tmid }, ref) => {
|
||||
const { showActionSheet } = useActionSheet();
|
||||
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
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;
|
||||
const handleResend = protectedFunction(async(message) => {
|
||||
await RocketChat.resendMessage(message, tmid);
|
||||
});
|
||||
|
||||
handleDelete = async() => {
|
||||
const handleDelete = async(message) => {
|
||||
try {
|
||||
const { message, tmid } = this.props;
|
||||
const db = database.active;
|
||||
const deleteBatch = [];
|
||||
const msgCollection = db.collections.get('messages');
|
||||
|
@ -49,7 +30,7 @@ class MessageErrorActions extends React.Component {
|
|||
try {
|
||||
const msg = await msgCollection.find(message.id);
|
||||
deleteBatch.push(msg.prepareDestroyPermanently());
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Do nothing: message not found
|
||||
}
|
||||
|
||||
|
@ -68,7 +49,7 @@ class MessageErrorActions extends React.Component {
|
|||
// If the whole thread was removed, delete the thread
|
||||
const thread = await threadCollection.find(tmid);
|
||||
deleteBatch.push(thread.prepareDestroyPermanently());
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Do nothing: thread not found
|
||||
}
|
||||
} else {
|
||||
|
@ -78,7 +59,7 @@ class MessageErrorActions extends React.Component {
|
|||
})
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Do nothing: message not found
|
||||
}
|
||||
}
|
||||
|
@ -88,39 +69,34 @@ class MessageErrorActions extends React.Component {
|
|||
} catch (e) {
|
||||
log(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
showActionSheet = () => {
|
||||
ActionSheet.showActionSheetWithOptions({
|
||||
options: this.options,
|
||||
cancelButtonIndex: this.CANCEL_INDEX,
|
||||
destructiveButtonIndex: this.DELETE_INDEX,
|
||||
title: I18n.t('Message_actions')
|
||||
}, (actionIndex) => {
|
||||
this.handleActionPress(actionIndex);
|
||||
const showMessageErrorActions = (message) => {
|
||||
showActionSheet({
|
||||
options: [
|
||||
{
|
||||
title: I18n.t('Resend'),
|
||||
icon: 'send',
|
||||
onPress: () => handleResend(message)
|
||||
},
|
||||
{
|
||||
title: I18n.t('Delete'),
|
||||
icon: 'trash',
|
||||
danger: true,
|
||||
onPress: () => handleDelete(message)
|
||||
}
|
||||
],
|
||||
hasCancel: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleActionPress = (actionIndex) => {
|
||||
const { actionsHide } = this.props;
|
||||
switch (actionIndex) {
|
||||
case this.RESEND_INDEX:
|
||||
this.handleResend();
|
||||
break;
|
||||
case this.DELETE_INDEX:
|
||||
this.handleDelete();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
actionsHide();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
showMessageErrorActions
|
||||
}));
|
||||
});
|
||||
MessageErrorActions.propTypes = {
|
||||
message: PropTypes.object,
|
||||
tmid: PropTypes.string
|
||||
};
|
||||
|
||||
export default MessageErrorActions;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
View, Text, FlatList, StyleSheet, SafeAreaView
|
||||
View, Text, FlatList, StyleSheet
|
||||
} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-native-modal';
|
||||
|
@ -12,8 +12,12 @@ import { CustomIcon } from '../lib/Icons';
|
|||
import sharedStyles from '../views/Styles';
|
||||
import { themes } from '../constants/colors';
|
||||
import { withTheme } from '../theme';
|
||||
import SafeAreaView from './SafeAreaView';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: {
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
titleContainer: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10
|
||||
|
@ -95,12 +99,12 @@ const ModalContent = React.memo(({
|
|||
}) => {
|
||||
if (message && message.reactions) {
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<SafeAreaView theme={props.theme} style={styles.safeArea}>
|
||||
<Touchable onPress={onClose}>
|
||||
<View style={styles.titleContainer}>
|
||||
<CustomIcon
|
||||
style={[styles.closeButton, { color: themes[props.theme].buttonText }]}
|
||||
name='cross'
|
||||
name='Cross'
|
||||
size={20}
|
||||
/>
|
||||
<Text style={[styles.title, { color: themes[props.theme].buttonText }]}>{I18n.t('Reactions')}</Text>
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import React from 'react';
|
||||
import { Image, StyleSheet } from 'react-native';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
import { STATUS_COLORS, themes } from '../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
style: {
|
||||
marginRight: 7,
|
||||
marginTop: 3
|
||||
},
|
||||
discussion: {
|
||||
marginRight: 6
|
||||
icon: {
|
||||
marginTop: 3,
|
||||
marginRight: 4
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -23,22 +20,32 @@ const RoomTypeIcon = React.memo(({
|
|||
|
||||
const color = themes[theme].auxiliaryText;
|
||||
|
||||
let icon = 'lock';
|
||||
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.
|
||||
return <CustomIcon name='chat' size={13} style={[styles.style, styles.iconColor, styles.discussion, { color }]} />;
|
||||
icon = 'chat';
|
||||
} 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 <Image source={{ uri: 'hashtag' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />;
|
||||
} if (type === 'd') {
|
||||
if (isGroupChat) {
|
||||
return <CustomIcon name='team' size={13} style={[styles.style, styles.discussion, { color }]} />;
|
||||
}
|
||||
return <CustomIcon name='at' size={13} style={[styles.style, styles.discussion, { color }]} />;
|
||||
} if (type === 'l') {
|
||||
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 }]} />;
|
||||
return (
|
||||
<CustomIcon
|
||||
name={icon}
|
||||
size={size}
|
||||
style={[
|
||||
type === 'l' && status ? { color: STATUS_COLORS[status] } : { color },
|
||||
styles.icon,
|
||||
style
|
||||
]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
RoomTypeIcon.propTypes = {
|
||||
|
@ -51,7 +58,7 @@ RoomTypeIcon.propTypes = {
|
|||
};
|
||||
|
||||
RoomTypeIcon.defaultProps = {
|
||||
size: 10
|
||||
size: 16
|
||||
};
|
||||
|
||||
export default RoomTypeIcon;
|
||||
|
|
|
@ -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;
|
|
@ -4,18 +4,21 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import { isIOS } from '../utils/deviceInfo';
|
||||
import { themes } from '../constants/colors';
|
||||
import { withTheme } from '../theme';
|
||||
|
||||
const StatusBar = React.memo(({ theme }) => {
|
||||
let barStyle = 'light-content';
|
||||
const StatusBar = React.memo(({ theme, barStyle, backgroundColor }) => {
|
||||
if (!barStyle) {
|
||||
barStyle = 'light-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 = {
|
||||
theme: PropTypes.string
|
||||
theme: PropTypes.string,
|
||||
barStyle: PropTypes.string,
|
||||
backgroundColor: PropTypes.string
|
||||
};
|
||||
|
||||
export default withTheme(StatusBar);
|
||||
export default StatusBar;
|
||||
|
|
|
@ -111,7 +111,7 @@ export default class RCTextInput extends React.PureComponent {
|
|||
return (
|
||||
<BorderlessButton onPress={this.tooglePassword} style={[styles.iconContainer, styles.iconRight]}>
|
||||
<CustomIcon
|
||||
name={showPassword ? 'Eye' : 'eye-off'}
|
||||
name={showPassword ? 'eye' : 'eye-off'}
|
||||
testID={testID ? `${ testID }-icon-right` : null}
|
||||
style={{ color: themes[theme].auxiliaryText }}
|
||||
size={20}
|
||||
|
|
|
@ -5,12 +5,12 @@ import PropTypes from 'prop-types';
|
|||
import { sha256 } from 'js-sha256';
|
||||
import Modal from 'react-native-modal';
|
||||
import useDeepCompareEffect from 'use-deep-compare-effect';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import TextInput from '../TextInput';
|
||||
import I18n from '../../i18n';
|
||||
import EventEmitter from '../../utils/events';
|
||||
import { withTheme } from '../../theme';
|
||||
import { withSplit } from '../../split';
|
||||
import { themes } from '../../constants/colors';
|
||||
import Button from '../Button';
|
||||
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 [data, setData] = useState({});
|
||||
const [code, setCode] = useState('');
|
||||
|
@ -93,7 +93,7 @@ const TwoFactor = React.memo(({ theme, split }) => {
|
|||
hideModalContentWhileAnimating
|
||||
>
|
||||
<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>
|
||||
{method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null}
|
||||
<TextInput
|
||||
|
@ -134,7 +134,11 @@ const TwoFactor = React.memo(({ theme, split }) => {
|
|||
});
|
||||
TwoFactor.propTypes = {
|
||||
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));
|
||||
|
|
|
@ -37,5 +37,8 @@ export default StyleSheet.create({
|
|||
buttonContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
tablet: {
|
||||
height: undefined
|
||||
}
|
||||
});
|
||||
|
|
|
@ -24,7 +24,7 @@ const Chip = ({
|
|||
<>
|
||||
{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>
|
||||
<CustomIcon name='cross' size={16} color={themes[theme].auxiliaryText} />
|
||||
<CustomIcon name='Cross' size={16} color={themes[theme].auxiliaryText} />
|
||||
</>
|
||||
</Touchable>
|
||||
);
|
||||
|
|
|
@ -22,7 +22,7 @@ const Input = ({
|
|||
{
|
||||
loading
|
||||
? <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>
|
||||
</Touchable>
|
||||
|
|
|
@ -55,7 +55,7 @@ export const Select = ({
|
|||
const Icon = () => (
|
||||
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 (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
View, StyleSheet, Text, Easing, Dimensions
|
||||
View, StyleSheet, Text, Easing
|
||||
} from 'react-native';
|
||||
import { Audio } from 'expo-av';
|
||||
import Slider from '@react-native-community/slider';
|
||||
|
@ -15,9 +15,9 @@ import { CustomIcon } from '../../lib/Icons';
|
|||
import sharedStyles from '../../views/Styles';
|
||||
import { themes } from '../../constants/colors';
|
||||
import { isAndroid, isIOS } from '../../utils/deviceInfo';
|
||||
import { withSplit } from '../../split';
|
||||
import MessageContext from './Context';
|
||||
import ActivityIndicator from '../ActivityIndicator';
|
||||
import { withDimensions } from '../../dimensions';
|
||||
|
||||
const mode = {
|
||||
allowsRecordingIOS: false,
|
||||
|
@ -98,8 +98,8 @@ class MessageAudio extends React.Component {
|
|||
static propTypes = {
|
||||
file: PropTypes.object.isRequired,
|
||||
theme: PropTypes.string,
|
||||
split: PropTypes.bool,
|
||||
getCustomEmoji: PropTypes.func
|
||||
getCustomEmoji: PropTypes.func,
|
||||
scale: PropTypes.number
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -138,7 +138,7 @@ class MessageAudio extends React.Component {
|
|||
const {
|
||||
currentTime, duration, paused, loading
|
||||
} = this.state;
|
||||
const { file, split, theme } = this.props;
|
||||
const { file, theme } = this.props;
|
||||
if (nextProps.theme !== theme) {
|
||||
return true;
|
||||
}
|
||||
|
@ -154,9 +154,6 @@ class MessageAudio extends React.Component {
|
|||
if (!equal(nextProps.file, file)) {
|
||||
return true;
|
||||
}
|
||||
if (nextProps.split !== split) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.loading !== loading) {
|
||||
return true;
|
||||
}
|
||||
|
@ -249,7 +246,7 @@ class MessageAudio extends React.Component {
|
|||
loading, paused, currentTime, duration
|
||||
} = this.state;
|
||||
const {
|
||||
file, getCustomEmoji, split, theme
|
||||
file, getCustomEmoji, theme, scale
|
||||
} = this.props;
|
||||
const { description } = file;
|
||||
const { baseUrl, user } = this.context;
|
||||
|
@ -263,8 +260,7 @@ class MessageAudio extends React.Component {
|
|||
<View
|
||||
style={[
|
||||
styles.audioContainer,
|
||||
{ backgroundColor: themes[theme].chatComponentBackground, borderColor: themes[theme].borderColor },
|
||||
split && sharedStyles.tabletContent
|
||||
{ backgroundColor: themes[theme].chatComponentBackground, borderColor: themes[theme].borderColor }
|
||||
]}
|
||||
>
|
||||
<Button loading={loading} paused={paused} onPress={this.togglePlayPause} theme={theme} />
|
||||
|
@ -279,7 +275,7 @@ class MessageAudio extends React.Component {
|
|||
minimumTrackTintColor={themes[theme].tintColor}
|
||||
maximumTrackTintColor={themes[theme].auxiliaryText}
|
||||
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>
|
||||
</View>
|
||||
|
@ -289,4 +285,4 @@ class MessageAudio extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default withSplit(MessageAudio);
|
||||
export default withDimensions(MessageAudio);
|
||||
|
|
|
@ -22,7 +22,7 @@ const CallButton = React.memo(({
|
|||
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>
|
||||
</>
|
||||
</Touchable>
|
||||
|
|
|
@ -10,19 +10,17 @@ import Touchable from './Touchable';
|
|||
import Markdown from '../markdown';
|
||||
import styles from './styles';
|
||||
import { formatAttachmentUrl } from '../../lib/utils';
|
||||
import { withSplit } from '../../split';
|
||||
import { themes } from '../../constants/colors';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import MessageContext from './Context';
|
||||
|
||||
const ImageProgress = createImageProgress(FastImage);
|
||||
|
||||
const Button = React.memo(({
|
||||
children, onPress, split, theme
|
||||
children, onPress, theme
|
||||
}) => (
|
||||
<Touchable
|
||||
onPress={onPress}
|
||||
style={[styles.imageContainer, split && sharedStyles.tabletContent]}
|
||||
style={styles.imageContainer}
|
||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||
>
|
||||
{children}
|
||||
|
@ -42,7 +40,7 @@ export const MessageImage = React.memo(({ img, theme }) => (
|
|||
));
|
||||
|
||||
const ImageContainer = React.memo(({
|
||||
file, imageUrl, showAttachment, getCustomEmoji, split, theme
|
||||
file, imageUrl, showAttachment, getCustomEmoji, theme
|
||||
}) => {
|
||||
const { baseUrl, user } = useContext(MessageContext);
|
||||
const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
|
||||
|
@ -54,7 +52,7 @@ const ImageContainer = React.memo(({
|
|||
|
||||
if (file.description) {
|
||||
return (
|
||||
<Button split={split} theme={theme} onPress={onPress}>
|
||||
<Button theme={theme} onPress={onPress}>
|
||||
<View>
|
||||
<MessageImage img={img} theme={theme} />
|
||||
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
|
||||
|
@ -64,19 +62,18 @@ const ImageContainer = React.memo(({
|
|||
}
|
||||
|
||||
return (
|
||||
<Button split={split} theme={theme} onPress={onPress}>
|
||||
<Button theme={theme} onPress={onPress}>
|
||||
<MessageImage img={img} theme={theme} />
|
||||
</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 = {
|
||||
file: PropTypes.object,
|
||||
imageUrl: PropTypes.string,
|
||||
showAttachment: PropTypes.func,
|
||||
theme: PropTypes.string,
|
||||
getCustomEmoji: PropTypes.func,
|
||||
split: PropTypes.bool
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
ImageContainer.displayName = 'MessageImageContainer';
|
||||
|
||||
|
@ -89,9 +86,8 @@ ImageContainer.displayName = 'MessageImage';
|
|||
Button.propTypes = {
|
||||
children: PropTypes.node,
|
||||
onPress: PropTypes.func,
|
||||
theme: PropTypes.string,
|
||||
split: PropTypes.bool
|
||||
theme: PropTypes.string
|
||||
};
|
||||
ImageContainer.displayName = 'MessageButton';
|
||||
|
||||
export default withSplit(ImageContainer);
|
||||
export default ImageContainer;
|
||||
|
|