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:
|
paths:
|
||||||
- vendor/bundle
|
- vendor/bundle
|
||||||
|
|
||||||
update-fastlane: &update-fastlane
|
update-fastlane-ios: &update-fastlane-ios
|
||||||
name: Update Fastlane
|
name: Update Fastlane
|
||||||
command: |
|
command: |
|
||||||
echo "ruby-2.6.4" > ~/.ruby-version
|
echo "ruby-2.6.4" > ~/.ruby-version
|
||||||
bundle install
|
bundle install
|
||||||
working_directory: ios
|
working_directory: ios
|
||||||
|
|
||||||
|
update-fastlane-android: &update-fastlane-android
|
||||||
|
name: Update Fastlane
|
||||||
|
command: |
|
||||||
|
echo "ruby-2.6.4" > ~/.ruby-version
|
||||||
|
bundle install
|
||||||
|
working_directory: android
|
||||||
|
|
||||||
|
save-gradle-cache: &save-gradle-cache
|
||||||
|
name: Save gradle cache
|
||||||
|
key: android-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
|
||||||
|
paths:
|
||||||
|
- ~/.gradle
|
||||||
|
|
||||||
|
restore_cache: &restore-gradle-cache
|
||||||
|
name: Restore gradle cache
|
||||||
|
key: android-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
|
||||||
|
|
||||||
restore-brew-cache: &restore-brew-cache
|
restore-brew-cache: &restore-brew-cache
|
||||||
name: Restore Brew cache
|
name: Restore Brew cache
|
||||||
key: brew-{{ checksum "yarn.lock" }}-{{ checksum ".circleci/config.yml" }}
|
key: brew-{{ checksum "yarn.lock" }}-{{ checksum ".circleci/config.yml" }}
|
||||||
|
@ -227,9 +244,9 @@ jobs:
|
||||||
|
|
||||||
- run: *install-npm-modules
|
- run: *install-npm-modules
|
||||||
|
|
||||||
- restore_cache:
|
- run: *update-fastlane-android
|
||||||
name: Restore gradle cache
|
|
||||||
key: android-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
|
- restore_cache: *restore-gradle-cache
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: Configure Gradle
|
name: Configure Gradle
|
||||||
|
@ -267,14 +284,10 @@ jobs:
|
||||||
name: Build Android App
|
name: Build Android App
|
||||||
command: |
|
command: |
|
||||||
if [[ $KEYSTORE ]]; then
|
if [[ $KEYSTORE ]]; then
|
||||||
./gradlew bundleRelease
|
bundle exec fastlane android release
|
||||||
else
|
else
|
||||||
./gradlew assembleDebug
|
bundle exec fastlane android build
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p /tmp/build
|
|
||||||
|
|
||||||
mv app/build/outputs /tmp/build/
|
|
||||||
working_directory: android
|
working_directory: android
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
|
@ -291,15 +304,40 @@ jobs:
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: /tmp/build/outputs
|
path: android/app/build/outputs
|
||||||
|
|
||||||
- save_cache: *save-npm-cache-linux
|
- save_cache: *save-npm-cache-linux
|
||||||
|
|
||||||
- save_cache:
|
- save_cache: *save-gradle-cache
|
||||||
name: Save gradle cache
|
|
||||||
key: android-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
|
- persist_to_workspace:
|
||||||
|
root: .
|
||||||
paths:
|
paths:
|
||||||
- ~/.gradle
|
- android/fastlane/report.xml
|
||||||
|
- android/app/build/outputs
|
||||||
|
|
||||||
|
android-google-play-alpha:
|
||||||
|
<<: *defaults
|
||||||
|
docker:
|
||||||
|
- image: circleci/android:api-28-node
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
|
||||||
|
- attach_workspace:
|
||||||
|
at: android
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Store the google service account key
|
||||||
|
command: echo "$FASTLANE_GOOGLE_SERVICE_ACCOUNT" | base64 --decode > service_account.json
|
||||||
|
working_directory: android
|
||||||
|
|
||||||
|
- run: *update-fastlane-android
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Fastlane Play Store Upload
|
||||||
|
command: bundle exec fastlane android alpha
|
||||||
|
working_directory: android
|
||||||
|
|
||||||
# iOS builds
|
# iOS builds
|
||||||
ios-build:
|
ios-build:
|
||||||
|
@ -316,7 +354,7 @@ jobs:
|
||||||
|
|
||||||
- run: *install-npm-modules
|
- run: *install-npm-modules
|
||||||
|
|
||||||
- run: *update-fastlane
|
- run: *update-fastlane-ios
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: Set Google Services
|
name: Set Google Services
|
||||||
|
@ -378,7 +416,7 @@ jobs:
|
||||||
|
|
||||||
- restore_cache: *restore-gems-cache
|
- restore_cache: *restore-gems-cache
|
||||||
|
|
||||||
- run: *update-fastlane
|
- run: *update-fastlane-ios
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: Fastlane Tesflight Upload
|
name: Fastlane Tesflight Upload
|
||||||
|
@ -424,3 +462,10 @@ workflows:
|
||||||
- android-build:
|
- android-build:
|
||||||
requires:
|
requires:
|
||||||
- lint-testunit
|
- lint-testunit
|
||||||
|
- android-hold-google-play-alpha:
|
||||||
|
type: approval
|
||||||
|
requires:
|
||||||
|
- android-build
|
||||||
|
- android-google-play-alpha:
|
||||||
|
requires:
|
||||||
|
- android-hold-google-play-alpha
|
||||||
|
|
|
@ -51,9 +51,10 @@ buck-out/
|
||||||
# For more information about the recommended setup visit:
|
# For more information about the recommended setup visit:
|
||||||
# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
|
# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
|
||||||
|
|
||||||
fastlane/report.xml
|
**/fastlane/report.xml
|
||||||
fastlane/Preview.html
|
**/fastlane/Preview.html
|
||||||
fastlane/screenshots
|
**/fastlane/screenshots
|
||||||
|
**/fastlane/test_output
|
||||||
|
|
||||||
coverage
|
coverage
|
||||||
|
|
||||||
|
|
|
@ -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
|
### Running single server
|
||||||
If you don't need multiple servers, there is a branch `single-server` just for that.
|
If you don't need multiple servers, there is a branch `single-server` just for that.
|
||||||
Readme will guide you on how to config.
|
Temp whitelabel docs: https://docs.google.com/document/d/17ib2Le_SH6U2gP0sEuKapF2J-WgqZxPFMRRVUSofz7Y/edit
|
||||||
|
|
||||||
## Current priorities
|
## Current priorities
|
||||||
1) Omnichannel support
|
1) Omnichannel support
|
||||||
|
|
|
@ -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">
|
package="chat.rocket.reactnative">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<!-- <uses-permission android:name="android.permission.CAMERA" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
||||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> -->
|
|
||||||
<!-- <uses-permission-sdk-23 android:name="android.permission.VIBRATE"/> -->
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".MainApplication"
|
android:name=".MainApplication"
|
||||||
|
@ -65,6 +60,7 @@
|
||||||
android:theme="@style/AppTheme" >
|
android:theme="@style/AppTheme" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="*/*" />
|
<data android:mimeType="*/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
|
@ -13,7 +13,8 @@ public class MainActivity extends ReactFragmentActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
// https://github.com/software-mansion/react-native-screens/issues/17#issuecomment-424704067
|
||||||
|
super.onCreate(null);
|
||||||
RNBootSplash.init(R.drawable.launch_screen, MainActivity.this);
|
RNBootSplash.init(R.drawable.launch_screen, MainActivity.this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ public class BasePackageList {
|
||||||
new expo.modules.keepawake.KeepAwakePackage(),
|
new expo.modules.keepawake.KeepAwakePackage(),
|
||||||
new expo.modules.localauthentication.LocalAuthenticationPackage(),
|
new expo.modules.localauthentication.LocalAuthenticationPackage(),
|
||||||
new expo.modules.permissions.PermissionsPackage(),
|
new expo.modules.permissions.PermissionsPackage(),
|
||||||
|
new expo.modules.videothumbnails.VideoThumbnailsPackage(),
|
||||||
new expo.modules.webbrowser.WebBrowserPackage()
|
new expo.modules.webbrowser.WebBrowserPackage()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
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
|
# Automatically convert third-party libraries to use AndroidX
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
APPLICATIONID=chat.rocket.reactnative
|
APPLICATIONID=chat.rocket.reactnative
|
||||||
VERSIONNAME=4.5.1
|
VERSIONNAME=4.8.0
|
||||||
VERSIONCODE=1
|
VERSIONCODE=1
|
||||||
BugsnagAPIKey=""
|
BugsnagAPIKey=""
|
||||||
KEYSTORE=my-upload-key.keystore
|
KEYSTORE=my-upload-key.keystore
|
||||||
|
|
|
@ -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', [
|
export const LOGIN = createRequestTypes('LOGIN', [
|
||||||
...defaultTypes,
|
...defaultTypes,
|
||||||
'SET_SERVICES',
|
'SET_SERVICES',
|
||||||
'SET_PREFERENCE'
|
'SET_PREFERENCE',
|
||||||
|
'SET_LOCAL_AUTHENTICATED'
|
||||||
]);
|
]);
|
||||||
export const SHARE = createRequestTypes('SHARE', [
|
export const SHARE = createRequestTypes('SHARE', [
|
||||||
'SELECT_SERVER',
|
'SELECT_SERVER',
|
||||||
|
@ -32,7 +33,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
|
||||||
'CLOSE_SEARCH_HEADER'
|
'CLOSE_SEARCH_HEADER'
|
||||||
]);
|
]);
|
||||||
export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'CLOSE', 'FORWARD', 'USER_TYPING']);
|
export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'CLOSE', 'FORWARD', 'USER_TYPING']);
|
||||||
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS']);
|
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS', 'SET_MASTER_DETAIL']);
|
||||||
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
|
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
|
||||||
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
|
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
|
||||||
export const CREATE_DISCUSSION = createRequestTypes('CREATE_DISCUSSION', [...defaultTypes]);
|
export const CREATE_DISCUSSION = createRequestTypes('CREATE_DISCUSSION', [...defaultTypes]);
|
||||||
|
@ -50,7 +51,6 @@ export const LOGOUT = 'LOGOUT'; // logout is always success
|
||||||
export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
|
export const SNIPPETED_MESSAGES = createRequestTypes('SNIPPETED_MESSAGES', ['OPEN', 'READY', 'CLOSE', 'MESSAGES_RECEIVED']);
|
||||||
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
|
export const DEEP_LINKING = createRequestTypes('DEEP_LINKING', ['OPEN']);
|
||||||
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);
|
export const SORT_PREFERENCES = createRequestTypes('SORT_PREFERENCES', ['SET_ALL', 'SET']);
|
||||||
export const NOTIFICATION = createRequestTypes('NOTIFICATION', ['RECEIVED', 'REMOVE']);
|
|
||||||
export const TOGGLE_CRASH_REPORT = 'TOGGLE_CRASH_REPORT';
|
export const TOGGLE_CRASH_REPORT = 'TOGGLE_CRASH_REPORT';
|
||||||
export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
|
export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
|
||||||
export const SET_ACTIVE_USERS = 'SET_ACTIVE_USERS';
|
export const SET_ACTIVE_USERS = 'SET_ACTIVE_USERS';
|
||||||
|
|
|
@ -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
|
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 {
|
return {
|
||||||
type: SERVER.INIT_ADD
|
type: SERVER.INIT_ADD,
|
||||||
|
previousServer
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/* eslint-disable no-bitwise */
|
/* eslint-disable no-bitwise */
|
||||||
import { constants } from 'react-native-keycommands';
|
import KeyCommands, { constants } from 'react-native-keycommands';
|
||||||
|
|
||||||
import I18n from './i18n';
|
import I18n from './i18n';
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ const KEY_ADD_SERVER = __DEV__ ? 'l' : 'n';
|
||||||
const KEY_SEND_MESSAGE = '\r';
|
const KEY_SEND_MESSAGE = '\r';
|
||||||
const KEY_SELECT = '123456789';
|
const KEY_SELECT = '123456789';
|
||||||
|
|
||||||
export const defaultCommands = [
|
const keyCommands = [
|
||||||
{
|
{
|
||||||
// Focus messageBox
|
// Focus messageBox
|
||||||
input: KEY_TYPING,
|
input: KEY_TYPING,
|
||||||
|
@ -29,10 +29,7 @@ export const defaultCommands = [
|
||||||
input: KEY_SEND_MESSAGE,
|
input: KEY_SEND_MESSAGE,
|
||||||
modifierFlags: 0,
|
modifierFlags: 0,
|
||||||
discoverabilityTitle: I18n.t('Send')
|
discoverabilityTitle: I18n.t('Send')
|
||||||
}
|
},
|
||||||
];
|
|
||||||
|
|
||||||
export const keyCommands = [
|
|
||||||
{
|
{
|
||||||
// Open Preferences Modal
|
// Open Preferences Modal
|
||||||
input: KEY_PREFERENCES,
|
input: KEY_PREFERENCES,
|
||||||
|
@ -139,6 +136,10 @@ export const keyCommands = [
|
||||||
})))
|
})))
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const setKeyCommands = () => KeyCommands.setKeyCommands(keyCommands);
|
||||||
|
|
||||||
|
export const deleteKeyCommands = () => KeyCommands.deleteKeyCommands(keyCommands);
|
||||||
|
|
||||||
export const KEY_COMMAND = 'KEY_COMMAND';
|
export const KEY_COMMAND = 'KEY_COMMAND';
|
||||||
|
|
||||||
export const commandHandle = (event, key, flags = []) => {
|
export const commandHandle = (event, key, flags = []) => {
|
||||||
|
|
|
@ -53,7 +53,9 @@ export const themes = {
|
||||||
passcodePrimary: '#2F343D',
|
passcodePrimary: '#2F343D',
|
||||||
passcodeSecondary: '#6C727A',
|
passcodeSecondary: '#6C727A',
|
||||||
passcodeDotEmpty: '#CBCED1',
|
passcodeDotEmpty: '#CBCED1',
|
||||||
passcodeDotFull: '#6C727A'
|
passcodeDotFull: '#6C727A',
|
||||||
|
previewBackground: '#1F2329',
|
||||||
|
previewTintColor: '#ffffff'
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
backgroundColor: '#030b1b',
|
backgroundColor: '#030b1b',
|
||||||
|
@ -95,7 +97,9 @@ export const themes = {
|
||||||
passcodePrimary: '#FFFFFF',
|
passcodePrimary: '#FFFFFF',
|
||||||
passcodeSecondary: '#CBCED1',
|
passcodeSecondary: '#CBCED1',
|
||||||
passcodeDotEmpty: '#CBCED1',
|
passcodeDotEmpty: '#CBCED1',
|
||||||
passcodeDotFull: '#6C727A'
|
passcodeDotFull: '#6C727A',
|
||||||
|
previewBackground: '#030b1b',
|
||||||
|
previewTintColor: '#ffffff'
|
||||||
},
|
},
|
||||||
black: {
|
black: {
|
||||||
backgroundColor: '#000000',
|
backgroundColor: '#000000',
|
||||||
|
@ -137,6 +141,8 @@ export const themes = {
|
||||||
passcodePrimary: '#FFFFFF',
|
passcodePrimary: '#FFFFFF',
|
||||||
passcodeSecondary: '#CBCED1',
|
passcodeSecondary: '#CBCED1',
|
||||||
passcodeDotEmpty: '#CBCED1',
|
passcodeDotEmpty: '#CBCED1',
|
||||||
passcodeDotFull: '#6C727A'
|
passcodeDotFull: '#6C727A',
|
||||||
|
previewBackground: '#000000',
|
||||||
|
previewTintColor: '#ffffff'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -50,6 +50,18 @@ export default {
|
||||||
Accounts_ManuallyApproveNewUsers: {
|
Accounts_ManuallyApproveNewUsers: {
|
||||||
type: 'valueAsBoolean'
|
type: 'valueAsBoolean'
|
||||||
},
|
},
|
||||||
|
API_Use_REST_For_DDP_Calls: {
|
||||||
|
type: 'valueAsBoolean'
|
||||||
|
},
|
||||||
|
Accounts_iframe_enabled: {
|
||||||
|
type: 'valueAsBoolean'
|
||||||
|
},
|
||||||
|
Accounts_Iframe_api_url: {
|
||||||
|
type: 'valueAsString'
|
||||||
|
},
|
||||||
|
Accounts_Iframe_api_method: {
|
||||||
|
type: 'valueAsString'
|
||||||
|
},
|
||||||
CROWD_Enable: {
|
CROWD_Enable: {
|
||||||
type: 'valueAsBoolean'
|
type: 'valueAsBoolean'
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
export const MAX_SIDEBAR_WIDTH = 321;
|
export const MAX_SIDEBAR_WIDTH = 321;
|
||||||
export const MAX_CONTENT_WIDTH = '90%';
|
|
||||||
export const MAX_SCREEN_CONTENT_WIDTH = '50%';
|
export const MAX_SCREEN_CONTENT_WIDTH = '50%';
|
||||||
export const MIN_WIDTH_SPLIT_LAYOUT = 700;
|
export const MIN_WIDTH_MASTER_DETAIL_LAYOUT = 700;
|
||||||
|
|
|
@ -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 React from 'react';
|
||||||
import { View, Image, StyleSheet } from 'react-native';
|
import { View, StyleSheet } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { themes } from '../constants/colors';
|
import { themes } from '../constants/colors';
|
||||||
|
import { CustomIcon } from '../lib/Icons';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
disclosureContainer: {
|
disclosureContainer: {
|
||||||
|
@ -10,17 +11,14 @@ const styles = StyleSheet.create({
|
||||||
marginRight: 9,
|
marginRight: 9,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
},
|
|
||||||
disclosureIndicator: {
|
|
||||||
width: 20,
|
|
||||||
height: 20
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const DisclosureImage = React.memo(({ theme }) => (
|
export const DisclosureImage = React.memo(({ theme }) => (
|
||||||
<Image
|
<CustomIcon
|
||||||
source={{ uri: 'disclosure_indicator' }}
|
name='chevron-right'
|
||||||
style={[styles.disclosureIndicator, { tintColor: themes[theme].auxiliaryTintColor }]}
|
color={themes[theme].auxiliaryTintColor}
|
||||||
|
size={20}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
DisclosureImage.propTypes = {
|
DisclosureImage.propTypes = {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Text, TouchableOpacity, FlatList } from 'react-native';
|
import { Text, TouchableOpacity, FlatList } from 'react-native';
|
||||||
import { responsive } from 'react-native-responsive-ui';
|
|
||||||
|
|
||||||
import shortnameToUnicode from '../../utils/shortnameToUnicode';
|
import shortnameToUnicode from '../../utils/shortnameToUnicode';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
@ -25,7 +24,6 @@ class EmojiCategory extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
baseUrl: PropTypes.string.isRequired,
|
baseUrl: PropTypes.string.isRequired,
|
||||||
emojis: PropTypes.any,
|
emojis: PropTypes.any,
|
||||||
window: PropTypes.any,
|
|
||||||
onEmojiSelected: PropTypes.func,
|
onEmojiSelected: PropTypes.func,
|
||||||
emojisPerRow: PropTypes.number,
|
emojisPerRow: PropTypes.number,
|
||||||
width: PropTypes.number
|
width: PropTypes.number
|
||||||
|
@ -73,4 +71,4 @@ class EmojiCategory extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default responsive(EmojiCategory);
|
export default EmojiCategory;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ScrollView, StyleSheet, View } from 'react-native';
|
import { ScrollView, StyleSheet, View } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { SafeAreaView } from 'react-navigation';
|
|
||||||
|
|
||||||
import { themes } from '../constants/colors';
|
import { themes } from '../constants/colors';
|
||||||
import sharedStyles from '../views/Styles';
|
import sharedStyles from '../views/Styles';
|
||||||
|
@ -10,6 +9,7 @@ import KeyboardView from '../presentation/KeyboardView';
|
||||||
import StatusBar from './StatusBar';
|
import StatusBar from './StatusBar';
|
||||||
import AppVersion from './AppVersion';
|
import AppVersion from './AppVersion';
|
||||||
import { isTablet } from '../utils/deviceInfo';
|
import { isTablet } from '../utils/deviceInfo';
|
||||||
|
import SafeAreaView from './SafeAreaView';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
scrollView: {
|
scrollView: {
|
||||||
|
@ -31,7 +31,7 @@ const FormContainer = ({ children, theme, testID }) => (
|
||||||
>
|
>
|
||||||
<StatusBar theme={theme} />
|
<StatusBar theme={theme} />
|
||||||
<ScrollView {...scrollPersistTaps} style={sharedStyles.container} contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}>
|
<ScrollView {...scrollPersistTaps} style={sharedStyles.container} contentContainerStyle={[sharedStyles.containerScrollView, styles.scrollView]}>
|
||||||
<SafeAreaView style={sharedStyles.container} forceInset={{ top: 'never' }} testID={testID}>
|
<SafeAreaView testID={testID} theme={theme} style={{ backgroundColor: themes[theme].backgroundColor }}>
|
||||||
{children}
|
{children}
|
||||||
<AppVersion theme={theme} />
|
<AppVersion theme={theme} />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|
|
@ -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>
|
</CustomHeaderButtons>
|
||||||
));
|
));
|
||||||
|
|
||||||
export const CloseModalButton = React.memo(({ navigation, testID, onPress = () => navigation.pop() }) => (
|
export const CloseModalButton = React.memo(({
|
||||||
|
navigation, testID, onPress = () => navigation.pop(), ...props
|
||||||
|
}) => (
|
||||||
<CustomHeaderButtons left>
|
<CustomHeaderButtons left>
|
||||||
<Item title='close' iconName='cross' onPress={onPress} testID={testID} />
|
<Item title='close' iconName='Cross' onPress={onPress} testID={testID} {...props} />
|
||||||
</CustomHeaderButtons>
|
</CustomHeaderButtons>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -46,7 +48,7 @@ export const CancelModalButton = React.memo(({ onPress, testID }) => (
|
||||||
<CustomHeaderButtons left>
|
<CustomHeaderButtons left>
|
||||||
{isIOS
|
{isIOS
|
||||||
? <Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} />
|
? <Item title={I18n.t('Cancel')} onPress={onPress} testID={testID} />
|
||||||
: <Item title='close' iconName='cross' onPress={onPress} testID={testID} />
|
: <Item title='close' iconName='Cross' onPress={onPress} testID={testID} />
|
||||||
}
|
}
|
||||||
</CustomHeaderButtons>
|
</CustomHeaderButtons>
|
||||||
));
|
));
|
||||||
|
@ -57,9 +59,9 @@ export const MoreButton = React.memo(({ onPress, testID }) => (
|
||||||
</CustomHeaderButtons>
|
</CustomHeaderButtons>
|
||||||
));
|
));
|
||||||
|
|
||||||
export const SaveButton = React.memo(({ onPress, testID }) => (
|
export const SaveButton = React.memo(({ onPress, testID, ...props }) => (
|
||||||
<CustomHeaderButtons>
|
<CustomHeaderButtons>
|
||||||
<Item title='save' iconName='Download' onPress={onPress} testID={testID} />
|
<Item title='save' iconName='download' onPress={onPress} testID={testID} {...props} />
|
||||||
</CustomHeaderButtons>
|
</CustomHeaderButtons>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -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 PropTypes from 'prop-types';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Base64 } from 'js-base64';
|
import { Base64 } from 'js-base64';
|
||||||
import { withNavigation } from 'react-navigation';
|
|
||||||
|
|
||||||
import { withTheme } from '../theme';
|
import { withTheme } from '../theme';
|
||||||
import sharedStyles from '../views/Styles';
|
import sharedStyles from '../views/Styles';
|
||||||
|
@ -361,4 +360,4 @@ const mapDispatchToProps = dispatch => ({
|
||||||
loginRequest: params => dispatch(loginRequestAction(params))
|
loginRequest: params => dispatch(loginRequestAction(params))
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(withNavigation(LoginServices)));
|
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(LoginServices));
|
||||||
|
|
|
@ -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 }
|
{ loading ? <ActivityIndicator theme={theme} /> : null }
|
||||||
</FastImage>
|
</FastImage>
|
||||||
)
|
)
|
||||||
: <CustomIcon name='file-generic' size={36} color={themes[theme].actionTintColor} />
|
: <CustomIcon name='clip' size={36} color={themes[theme].actionTintColor} />
|
||||||
}
|
}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,22 +1,28 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
|
||||||
import { CancelEditingButton, ActionsButton } from './buttons';
|
import { CancelEditingButton, ActionsButton } from './buttons';
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
const LeftButtons = React.memo(({
|
const LeftButtons = React.memo(({
|
||||||
theme, showMessageBoxActions, editing, editCancel
|
theme, showMessageBoxActions, editing, editCancel, isActionsEnabled
|
||||||
}) => {
|
}) => {
|
||||||
if (editing) {
|
if (editing) {
|
||||||
return <CancelEditingButton onPress={editCancel} theme={theme} />;
|
return <CancelEditingButton onPress={editCancel} theme={theme} />;
|
||||||
}
|
}
|
||||||
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
|
if (isActionsEnabled) {
|
||||||
|
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
|
||||||
|
}
|
||||||
|
return <View style={styles.buttonsWhitespace} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
LeftButtons.propTypes = {
|
LeftButtons.propTypes = {
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
showMessageBoxActions: PropTypes.func.isRequired,
|
showMessageBoxActions: PropTypes.func.isRequired,
|
||||||
editing: PropTypes.bool,
|
editing: PropTypes.bool,
|
||||||
editCancel: PropTypes.func.isRequired
|
editCancel: PropTypes.func.isRequired,
|
||||||
|
isActionsEnabled: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LeftButtons;
|
export default LeftButtons;
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {
|
||||||
View, SafeAreaView, PermissionsAndroid, Text
|
View, PermissionsAndroid, Text
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { AudioRecorder, AudioUtils } from 'react-native-audio';
|
import { AudioRecorder, AudioUtils } from 'react-native-audio';
|
||||||
import { BorderlessButton } from 'react-native-gesture-handler';
|
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||||
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
|
import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake';
|
||||||
import RNFetchBlob from 'rn-fetch-blob';
|
import * as FileSystem from 'expo-file-system';
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import { isIOS, isAndroid } from '../../utils/deviceInfo';
|
import { isIOS, isAndroid } from '../../utils/deviceInfo';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
|
import SafeAreaView from '../SafeAreaView';
|
||||||
|
|
||||||
export const _formatTime = function(seconds) {
|
export const _formatTime = function(seconds) {
|
||||||
let minutes = Math.floor(seconds / 60);
|
let minutes = Math.floor(seconds / 60);
|
||||||
|
@ -93,9 +94,6 @@ export default class extends React.PureComponent {
|
||||||
if (!didSucceed) {
|
if (!didSucceed) {
|
||||||
return onFinish && onFinish(didSucceed);
|
return onFinish && onFinish(didSucceed);
|
||||||
}
|
}
|
||||||
if (isAndroid) {
|
|
||||||
filePath = filePath.startsWith('file://') ? filePath : `file://${ filePath }`;
|
|
||||||
}
|
|
||||||
const fileInfo = {
|
const fileInfo = {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
mime: 'audio/aac',
|
mime: 'audio/aac',
|
||||||
|
@ -110,9 +108,10 @@ export default class extends React.PureComponent {
|
||||||
finishAudioMessage = async() => {
|
finishAudioMessage = async() => {
|
||||||
try {
|
try {
|
||||||
this.recording = false;
|
this.recording = false;
|
||||||
const filePath = await AudioRecorder.stopRecording();
|
let filePath = await AudioRecorder.stopRecording();
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
const data = await RNFetchBlob.fs.stat(decodeURIComponent(filePath));
|
filePath = filePath.startsWith('file://') ? filePath : `file://${ filePath }`;
|
||||||
|
const data = await FileSystem.getInfoAsync(decodeURIComponent(filePath), { size: true });
|
||||||
this.finishRecording(true, filePath, data.size);
|
this.finishRecording(true, filePath, data.size);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -134,6 +133,7 @@ export default class extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
testID='messagebox-recording'
|
testID='messagebox-recording'
|
||||||
|
theme={theme}
|
||||||
style={[
|
style={[
|
||||||
styles.textBox,
|
styles.textBox,
|
||||||
{ borderTopColor: themes[theme].borderColor }
|
{ borderTopColor: themes[theme].borderColor }
|
||||||
|
@ -149,7 +149,7 @@ export default class extends React.PureComponent {
|
||||||
<CustomIcon
|
<CustomIcon
|
||||||
size={22}
|
size={22}
|
||||||
color={themes[theme].dangerColor}
|
color={themes[theme].dangerColor}
|
||||||
name='cross'
|
name='Cross'
|
||||||
/>
|
/>
|
||||||
</BorderlessButton>
|
</BorderlessButton>
|
||||||
<Text key='currentTime' style={[styles.textBoxInput, { color: themes[theme].titleText }]}>{currentTime}</Text>
|
<Text key='currentTime' style={[styles.textBoxInput, { color: themes[theme].titleText }]}>{currentTime}</Text>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { View, Text, StyleSheet } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
|
|
||||||
import Markdown from '../markdown';
|
import Markdown from '../markdown';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
|
@ -58,7 +59,7 @@ const ReplyPreview = React.memo(({
|
||||||
>
|
>
|
||||||
<View style={[styles.messageContainer, { backgroundColor: themes[theme].chatComponentBackground }]}>
|
<View style={[styles.messageContainer, { backgroundColor: themes[theme].chatComponentBackground }]}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={[styles.username, { color: themes[theme].tintColor }]}>{message.u.username}</Text>
|
<Text style={[styles.username, { color: themes[theme].tintColor }]}>{message.u?.username}</Text>
|
||||||
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Markdown
|
<Markdown
|
||||||
|
@ -71,10 +72,10 @@ const ReplyPreview = React.memo(({
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<CustomIcon name='cross' color={themes[theme].auxiliaryText} size={20} style={styles.close} onPress={close} />
|
<CustomIcon name='Cross' color={themes[theme].auxiliaryText} size={20} style={styles.close} onPress={close} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}, (prevProps, nextProps) => prevProps.replying === nextProps.replying && prevProps.theme === nextProps.theme);
|
}, (prevProps, nextProps) => prevProps.replying === nextProps.replying && prevProps.theme === nextProps.theme && isEqual(prevProps.message, nextProps.message));
|
||||||
|
|
||||||
ReplyPreview.propTypes = {
|
ReplyPreview.propTypes = {
|
||||||
replying: PropTypes.bool,
|
replying: PropTypes.bool,
|
||||||
|
|
|
@ -1,23 +1,25 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
|
||||||
import { SendButton, AudioButton, ActionsButton } from './buttons';
|
import { SendButton, AudioButton, ActionsButton } from './buttons';
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
const RightButtons = React.memo(({
|
const RightButtons = React.memo(({
|
||||||
theme, showSend, submit, recordAudioMessage, recordAudioMessageEnabled, showMessageBoxActions
|
theme, showSend, submit, recordAudioMessage, recordAudioMessageEnabled, showMessageBoxActions, isActionsEnabled
|
||||||
}) => {
|
}) => {
|
||||||
if (showSend) {
|
if (showSend) {
|
||||||
return <SendButton onPress={submit} theme={theme} />;
|
return <SendButton onPress={submit} theme={theme} />;
|
||||||
}
|
}
|
||||||
if (recordAudioMessageEnabled) {
|
if (recordAudioMessageEnabled || isActionsEnabled) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AudioButton onPress={recordAudioMessage} theme={theme} />
|
{recordAudioMessageEnabled ? <AudioButton onPress={recordAudioMessage} theme={theme} /> : null}
|
||||||
<ActionsButton onPress={showMessageBoxActions} theme={theme} />
|
{isActionsEnabled ? <ActionsButton onPress={showMessageBoxActions} theme={theme} /> : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <ActionsButton onPress={showMessageBoxActions} theme={theme} />;
|
return <View style={styles.buttonsWhitespace} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
RightButtons.propTypes = {
|
RightButtons.propTypes = {
|
||||||
|
@ -26,7 +28,8 @@ RightButtons.propTypes = {
|
||||||
submit: PropTypes.func.isRequired,
|
submit: PropTypes.func.isRequired,
|
||||||
recordAudioMessage: PropTypes.func.isRequired,
|
recordAudioMessage: PropTypes.func.isRequired,
|
||||||
recordAudioMessageEnabled: PropTypes.bool,
|
recordAudioMessageEnabled: PropTypes.bool,
|
||||||
showMessageBoxActions: PropTypes.func.isRequired
|
showMessageBoxActions: PropTypes.func.isRequired,
|
||||||
|
isActionsEnabled: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RightButtons;
|
export default RightButtons;
|
||||||
|
|
|
@ -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)}
|
accessibilityLabel={I18n.t(accessibilityLabel)}
|
||||||
accessibilityTraits='button'
|
accessibilityTraits='button'
|
||||||
>
|
>
|
||||||
<CustomIcon name={icon} size={23} color={themes[theme].tintColor} />
|
<CustomIcon name={icon} size={25} color={themes[theme].tintColor} />
|
||||||
</BorderlessButton>
|
</BorderlessButton>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ const CancelEditingButton = React.memo(({ theme, onPress }) => (
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
testID='messagebox-cancel-editing'
|
testID='messagebox-cancel-editing'
|
||||||
accessibilityLabel='Cancel_editing'
|
accessibilityLabel='Cancel_editing'
|
||||||
icon='cross'
|
icon='Cross'
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
|
@ -8,7 +8,7 @@ const SendButton = React.memo(({ theme, onPress }) => (
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
testID='messagebox-send-message'
|
testID='messagebox-send-message'
|
||||||
accessibilityLabel='Send_message'
|
accessibilityLabel='Send_message'
|
||||||
icon='Send-active'
|
icon='send-active'
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { View, Alert, Keyboard } from 'react-native';
|
import {
|
||||||
|
View, Alert, Keyboard, NativeModules
|
||||||
|
} from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { KeyboardAccessoryView } from 'react-native-keyboard-input';
|
import { KeyboardAccessoryView } from 'react-native-keyboard-input';
|
||||||
import ImagePicker from 'react-native-image-crop-picker';
|
import ImagePicker from 'react-native-image-crop-picker';
|
||||||
import equal from 'deep-equal';
|
import equal from 'deep-equal';
|
||||||
import DocumentPicker from 'react-native-document-picker';
|
import DocumentPicker from 'react-native-document-picker';
|
||||||
import ActionSheet from 'react-native-action-sheet';
|
|
||||||
import { Q } from '@nozbe/watermelondb';
|
import { Q } from '@nozbe/watermelondb';
|
||||||
|
|
||||||
import { generateTriggerId } from '../../lib/methods/actions';
|
import { generateTriggerId } from '../../lib/methods/actions';
|
||||||
|
@ -17,7 +18,6 @@ import styles from './styles';
|
||||||
import database from '../../lib/database';
|
import database from '../../lib/database';
|
||||||
import { emojis } from '../../emojis';
|
import { emojis } from '../../emojis';
|
||||||
import Recording from './Recording';
|
import Recording from './Recording';
|
||||||
import UploadModal from './UploadModal';
|
|
||||||
import log from '../../utils/log';
|
import log from '../../utils/log';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import ReplyPreview from './ReplyPreview';
|
import ReplyPreview from './ReplyPreview';
|
||||||
|
@ -43,9 +43,9 @@ import {
|
||||||
MENTIONS_TRACKING_TYPE_USERS
|
MENTIONS_TRACKING_TYPE_USERS
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import CommandsPreview from './CommandsPreview';
|
import CommandsPreview from './CommandsPreview';
|
||||||
import { Review } from '../../utils/review';
|
|
||||||
import { getUserSelector } from '../../selectors/login';
|
import { getUserSelector } from '../../selectors/login';
|
||||||
import Navigation from '../../lib/Navigation';
|
import Navigation from '../../lib/Navigation';
|
||||||
|
import { withActionSheet } from '../ActionSheet';
|
||||||
|
|
||||||
const imagePickerConfig = {
|
const imagePickerConfig = {
|
||||||
cropping: true,
|
cropping: true,
|
||||||
|
@ -54,6 +54,7 @@ const imagePickerConfig = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const libraryPickerConfig = {
|
const libraryPickerConfig = {
|
||||||
|
multiple: true,
|
||||||
mediaType: 'any'
|
mediaType: 'any'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -61,13 +62,6 @@ const videoPickerConfig = {
|
||||||
mediaType: 'video'
|
mediaType: 'video'
|
||||||
};
|
};
|
||||||
|
|
||||||
const FILE_CANCEL_INDEX = 0;
|
|
||||||
const FILE_PHOTO_INDEX = 1;
|
|
||||||
const FILE_VIDEO_INDEX = 2;
|
|
||||||
const FILE_LIBRARY_INDEX = 3;
|
|
||||||
const FILE_DOCUMENT_INDEX = 4;
|
|
||||||
const CREATE_DISCUSSION_INDEX = 5;
|
|
||||||
|
|
||||||
class MessageBox extends Component {
|
class MessageBox extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
rid: PropTypes.string.isRequired,
|
rid: PropTypes.string.isRequired,
|
||||||
|
@ -95,7 +89,24 @@ class MessageBox extends Component {
|
||||||
typing: PropTypes.func,
|
typing: PropTypes.func,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
replyCancel: PropTypes.func,
|
replyCancel: PropTypes.func,
|
||||||
navigation: PropTypes.object
|
showSend: PropTypes.bool,
|
||||||
|
navigation: PropTypes.object,
|
||||||
|
children: PropTypes.node,
|
||||||
|
isMasterDetail: PropTypes.bool,
|
||||||
|
showActionSheet: PropTypes.func,
|
||||||
|
iOSScrollBehavior: PropTypes.number,
|
||||||
|
sharing: PropTypes.bool,
|
||||||
|
isActionsEnabled: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
message: {
|
||||||
|
id: ''
|
||||||
|
},
|
||||||
|
sharing: false,
|
||||||
|
iOSScrollBehavior: NativeModules.KeyboardTrackingViewManager?.KeyboardTrackingScrollBehaviorFixedOffset,
|
||||||
|
isActionsEnabled: true,
|
||||||
|
getCustomEmoji: () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -103,26 +114,45 @@ class MessageBox extends Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
mentions: [],
|
mentions: [],
|
||||||
showEmojiKeyboard: false,
|
showEmojiKeyboard: false,
|
||||||
showSend: false,
|
showSend: props.showSend,
|
||||||
recording: false,
|
recording: false,
|
||||||
trackingType: '',
|
trackingType: '',
|
||||||
file: {
|
|
||||||
isVisible: false
|
|
||||||
},
|
|
||||||
commandPreview: [],
|
commandPreview: [],
|
||||||
showCommandPreview: false,
|
showCommandPreview: false,
|
||||||
command: {}
|
command: {}
|
||||||
};
|
};
|
||||||
this.text = '';
|
this.text = '';
|
||||||
this.focused = false;
|
this.focused = false;
|
||||||
this.messageBoxActions = [
|
|
||||||
I18n.t('Cancel'),
|
// MessageBox Actions
|
||||||
I18n.t('Take_a_photo'),
|
this.options = [
|
||||||
I18n.t('Take_a_video'),
|
{
|
||||||
I18n.t('Choose_from_library'),
|
title: I18n.t('Take_a_photo'),
|
||||||
I18n.t('Choose_file'),
|
icon: 'image',
|
||||||
I18n.t('Create_Discussion')
|
onPress: this.takePhoto
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: I18n.t('Take_a_video'),
|
||||||
|
icon: 'video-1',
|
||||||
|
onPress: this.takeVideo
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: I18n.t('Choose_from_library'),
|
||||||
|
icon: 'share',
|
||||||
|
onPress: this.chooseFromLibrary
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: I18n.t('Choose_file'),
|
||||||
|
icon: 'folder',
|
||||||
|
onPress: this.chooseFile
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: I18n.t('Create_Discussion'),
|
||||||
|
icon: 'chat',
|
||||||
|
onPress: this.createDiscussion
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const libPickerLabels = {
|
const libPickerLabels = {
|
||||||
cropperChooseText: I18n.t('Choose'),
|
cropperChooseText: I18n.t('Choose'),
|
||||||
cropperCancelText: I18n.t('Cancel'),
|
cropperCancelText: I18n.t('Cancel'),
|
||||||
|
@ -144,27 +174,29 @@ class MessageBox extends Component {
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const { rid, tmid, navigation } = this.props;
|
const {
|
||||||
|
rid, tmid, navigation, sharing
|
||||||
|
} = this.props;
|
||||||
let msg;
|
let msg;
|
||||||
try {
|
try {
|
||||||
const threadsCollection = db.collections.get('threads');
|
const threadsCollection = db.collections.get('threads');
|
||||||
const subsCollection = db.collections.get('subscriptions');
|
const subsCollection = db.collections.get('subscriptions');
|
||||||
|
try {
|
||||||
|
this.room = await subsCollection.find(rid);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Messagebox.didMount: Room not found');
|
||||||
|
}
|
||||||
if (tmid) {
|
if (tmid) {
|
||||||
try {
|
try {
|
||||||
const thread = await threadsCollection.find(tmid);
|
this.thread = await threadsCollection.find(tmid);
|
||||||
if (thread) {
|
if (this.thread && !sharing) {
|
||||||
msg = thread.draftMessage;
|
msg = this.thread.draftMessage;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Messagebox.didMount: Thread not found');
|
console.log('Messagebox.didMount: Thread not found');
|
||||||
}
|
}
|
||||||
} else {
|
} else if (!sharing) {
|
||||||
try {
|
msg = this.room?.draftMessage;
|
||||||
this.room = await subsCollection.find(rid);
|
|
||||||
msg = this.room.draftMessage;
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Messagebox.didMount: Room not found');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e);
|
log(e);
|
||||||
|
@ -183,16 +215,25 @@ class MessageBox extends Component {
|
||||||
EventEmiter.addEventListener(KEY_COMMAND, this.handleCommands);
|
EventEmiter.addEventListener(KEY_COMMAND, this.handleCommands);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.didFocusListener = navigation.addListener('didFocus', () => {
|
this.unsubscribeFocus = navigation.addListener('focus', () => {
|
||||||
if (this.tracking && this.tracking.resetTracking) {
|
if (this.tracking && this.tracking.resetTracking) {
|
||||||
this.tracking.resetTracking();
|
this.tracking.resetTracking();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.unsubscribeBlur = navigation.addListener('blur', () => {
|
||||||
|
this.component?.blur();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
const { isFocused, editing, replying } = this.props;
|
const {
|
||||||
if (!isFocused()) {
|
isFocused, editing, replying, sharing
|
||||||
|
} = this.props;
|
||||||
|
if (!isFocused?.()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sharing) {
|
||||||
|
this.setInput(nextProps.message.msg ?? '');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (editing !== nextProps.editing && nextProps.editing) {
|
if (editing !== nextProps.editing && nextProps.editing) {
|
||||||
|
@ -200,6 +241,7 @@ class MessageBox extends Component {
|
||||||
if (this.text) {
|
if (this.text) {
|
||||||
this.setShowSend(true);
|
this.setShowSend(true);
|
||||||
}
|
}
|
||||||
|
this.focus();
|
||||||
} else if (replying !== nextProps.replying && nextProps.replying) {
|
} else if (replying !== nextProps.replying && nextProps.replying) {
|
||||||
this.focus();
|
this.focus();
|
||||||
} else if (!nextProps.message) {
|
} else if (!nextProps.message) {
|
||||||
|
@ -209,11 +251,11 @@ class MessageBox extends Component {
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const {
|
const {
|
||||||
showEmojiKeyboard, showSend, recording, mentions, file, commandPreview
|
showEmojiKeyboard, showSend, recording, mentions, commandPreview
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
roomType, replying, editing, isFocused, theme
|
roomType, replying, editing, isFocused, message, theme, children
|
||||||
} = this.props;
|
} = this.props;
|
||||||
if (nextProps.theme !== theme) {
|
if (nextProps.theme !== theme) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -245,7 +287,10 @@ class MessageBox extends Component {
|
||||||
if (!equal(nextState.commandPreview, commandPreview)) {
|
if (!equal(nextState.commandPreview, commandPreview)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!equal(nextState.file, file)) {
|
if (!equal(nextProps.message, message)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!equal(nextProps.children, children)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -268,8 +313,11 @@ class MessageBox extends Component {
|
||||||
if (this.getSlashCommands && this.getSlashCommands.stop) {
|
if (this.getSlashCommands && this.getSlashCommands.stop) {
|
||||||
this.getSlashCommands.stop();
|
this.getSlashCommands.stop();
|
||||||
}
|
}
|
||||||
if (this.didFocusListener && this.didFocusListener.remove) {
|
if (this.unsubscribeFocus) {
|
||||||
this.didFocusListener.remove();
|
this.unsubscribeFocus();
|
||||||
|
}
|
||||||
|
if (this.unsubscribeBlur) {
|
||||||
|
this.unsubscribeBlur();
|
||||||
}
|
}
|
||||||
if (isTablet) {
|
if (isTablet) {
|
||||||
EventEmiter.removeListener(KEY_COMMAND, this.handleCommands);
|
EventEmiter.removeListener(KEY_COMMAND, this.handleCommands);
|
||||||
|
@ -285,22 +333,26 @@ class MessageBox extends Component {
|
||||||
|
|
||||||
// eslint-disable-next-line react/sort-comp
|
// eslint-disable-next-line react/sort-comp
|
||||||
debouncedOnChangeText = debounce(async(text) => {
|
debouncedOnChangeText = debounce(async(text) => {
|
||||||
|
const { sharing } = this.props;
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const isTextEmpty = text.length === 0;
|
const isTextEmpty = text.length === 0;
|
||||||
// this.setShowSend(!isTextEmpty);
|
// this.setShowSend(!isTextEmpty);
|
||||||
this.handleTyping(!isTextEmpty);
|
this.handleTyping(!isTextEmpty);
|
||||||
// matches if their is text that stats with '/' and group the command and params so we can use it "/command params"
|
|
||||||
const slashCommand = text.match(/^\/([a-z0-9._-]+) (.+)/im);
|
if (!sharing) {
|
||||||
if (slashCommand) {
|
// matches if their is text that stats with '/' and group the command and params so we can use it "/command params"
|
||||||
const [, name, params] = slashCommand;
|
const slashCommand = text.match(/^\/([a-z0-9._-]+) (.+)/im);
|
||||||
const commandsCollection = db.collections.get('slash_commands');
|
if (slashCommand) {
|
||||||
try {
|
const [, name, params] = slashCommand;
|
||||||
const command = await commandsCollection.find(name);
|
const commandsCollection = db.collections.get('slash_commands');
|
||||||
if (command.providesPreview) {
|
try {
|
||||||
return this.setCommandPreview(command, name, params);
|
const command = await commandsCollection.find(name);
|
||||||
|
if (command.providesPreview) {
|
||||||
|
return this.setCommandPreview(command, name, params);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Slash command not found');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.log('Slash command not found');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,12 +362,20 @@ class MessageBox extends Component {
|
||||||
const cursor = Math.max(start, end);
|
const cursor = Math.max(start, end);
|
||||||
const lastNativeText = this.component?.lastNativeText || '';
|
const lastNativeText = this.component?.lastNativeText || '';
|
||||||
// matches if text either starts with '/' or have (@,#,:) then it groups whatever comes next of mention type
|
// matches if text either starts with '/' or have (@,#,:) then it groups whatever comes next of mention type
|
||||||
const regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im;
|
let regexp = /(#|@|:|^\/)([a-z0-9._-]+)$/im;
|
||||||
|
|
||||||
|
// if sharing, track #|@|:
|
||||||
|
if (sharing) {
|
||||||
|
regexp = /(#|@|:)([a-z0-9._-]+)$/im;
|
||||||
|
}
|
||||||
|
|
||||||
const result = lastNativeText.substr(0, cursor).match(regexp);
|
const result = lastNativeText.substr(0, cursor).match(regexp);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
const slash = lastNativeText.match(/^\/$/); // matches only '/' in input
|
if (!sharing) {
|
||||||
if (slash) {
|
const slash = lastNativeText.match(/^\/$/); // matches only '/' in input
|
||||||
return this.identifyMentionKeyword('', MENTIONS_TRACKING_TYPE_COMMANDS);
|
if (slash) {
|
||||||
|
return this.identifyMentionKeyword('', MENTIONS_TRACKING_TYPE_COMMANDS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return this.stopTrackingMention();
|
return this.stopTrackingMention();
|
||||||
}
|
}
|
||||||
|
@ -455,7 +515,10 @@ class MessageBox extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTyping = (isTyping) => {
|
handleTyping = (isTyping) => {
|
||||||
const { typing, rid } = this.props;
|
const { typing, rid, sharing } = this.props;
|
||||||
|
if (sharing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!isTyping) {
|
if (!isTyping) {
|
||||||
if (this.typingTimeout) {
|
if (this.typingTimeout) {
|
||||||
clearTimeout(this.typingTimeout);
|
clearTimeout(this.typingTimeout);
|
||||||
|
@ -495,7 +558,8 @@ class MessageBox extends Component {
|
||||||
|
|
||||||
setShowSend = (showSend) => {
|
setShowSend = (showSend) => {
|
||||||
const { showSend: prevShowSend } = this.state;
|
const { showSend: prevShowSend } = this.state;
|
||||||
if (prevShowSend !== showSend) {
|
const { showSend: propShowSend } = this.props;
|
||||||
|
if (prevShowSend !== showSend && !propShowSend) {
|
||||||
this.setState({ showSend });
|
this.setState({ showSend });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -507,7 +571,7 @@ class MessageBox extends Component {
|
||||||
|
|
||||||
canUploadFile = (file) => {
|
canUploadFile = (file) => {
|
||||||
const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = this.props;
|
const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = this.props;
|
||||||
const result = canUploadFile(file, { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize });
|
const result = canUploadFile(file, FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -515,33 +579,11 @@ class MessageBox extends Component {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMediaMessage = async(file) => {
|
|
||||||
const {
|
|
||||||
rid, tmid, baseUrl: server, user, message: { id: messageTmid }, replyCancel
|
|
||||||
} = this.props;
|
|
||||||
this.setState({ file: { isVisible: false } });
|
|
||||||
const fileInfo = {
|
|
||||||
name: file.name,
|
|
||||||
description: file.description,
|
|
||||||
size: file.size,
|
|
||||||
type: file.mime,
|
|
||||||
store: 'Uploads',
|
|
||||||
path: file.path
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
replyCancel();
|
|
||||||
await RocketChat.sendFileMessage(rid, fileInfo, tmid || messageTmid, server, user);
|
|
||||||
Review.pushPositiveEvent();
|
|
||||||
} catch (e) {
|
|
||||||
log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
takePhoto = async() => {
|
takePhoto = async() => {
|
||||||
try {
|
try {
|
||||||
const image = await ImagePicker.openCamera(this.imagePickerConfig);
|
const image = await ImagePicker.openCamera(this.imagePickerConfig);
|
||||||
if (this.canUploadFile(image)) {
|
if (this.canUploadFile(image)) {
|
||||||
this.showUploadModal(image);
|
this.openShareView([image]);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
|
@ -552,7 +594,7 @@ class MessageBox extends Component {
|
||||||
try {
|
try {
|
||||||
const video = await ImagePicker.openCamera(this.videoPickerConfig);
|
const video = await ImagePicker.openCamera(this.videoPickerConfig);
|
||||||
if (this.canUploadFile(video)) {
|
if (this.canUploadFile(video)) {
|
||||||
this.showUploadModal(video);
|
this.openShareView([video]);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
|
@ -561,10 +603,8 @@ class MessageBox extends Component {
|
||||||
|
|
||||||
chooseFromLibrary = async() => {
|
chooseFromLibrary = async() => {
|
||||||
try {
|
try {
|
||||||
const image = await ImagePicker.openPicker(this.libraryPickerConfig);
|
const attachments = await ImagePicker.openPicker(this.libraryPickerConfig);
|
||||||
if (this.canUploadFile(image)) {
|
this.openShareView(attachments);
|
||||||
this.showUploadModal(image);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
@ -582,7 +622,7 @@ class MessageBox extends Component {
|
||||||
path: res.uri
|
path: res.uri
|
||||||
};
|
};
|
||||||
if (this.canUploadFile(file)) {
|
if (this.canUploadFile(file)) {
|
||||||
this.showUploadModal(file);
|
this.openShareView([file]);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!DocumentPicker.isCancel(e)) {
|
if (!DocumentPicker.isCancel(e)) {
|
||||||
|
@ -591,43 +631,30 @@ class MessageBox extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createDiscussion = () => {
|
openShareView = (attachments) => {
|
||||||
Navigation.navigate('CreateDiscussionView', { channel: this.room });
|
const { message, replyCancel, replyWithMention } = this.props;
|
||||||
|
// Start a thread with an attachment
|
||||||
|
let { thread } = this;
|
||||||
|
if (replyWithMention) {
|
||||||
|
thread = message;
|
||||||
|
replyCancel();
|
||||||
|
}
|
||||||
|
Navigation.navigate('ShareView', { room: this.room, thread, attachments });
|
||||||
}
|
}
|
||||||
|
|
||||||
showUploadModal = (file) => {
|
createDiscussion = () => {
|
||||||
this.setState({ file: { ...file, isVisible: true } });
|
const { isMasterDetail } = this.props;
|
||||||
|
const params = { channel: this.room, showCloseModal: true };
|
||||||
|
if (isMasterDetail) {
|
||||||
|
Navigation.navigate('ModalStackNavigator', { screen: 'CreateDiscussionView', params });
|
||||||
|
} else {
|
||||||
|
Navigation.navigate('NewMessageStackNavigator', { screen: 'CreateDiscussionView', params });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showMessageBoxActions = () => {
|
showMessageBoxActions = () => {
|
||||||
ActionSheet.showActionSheetWithOptions({
|
const { showActionSheet } = this.props;
|
||||||
options: this.messageBoxActions,
|
showActionSheet({ options: this.options });
|
||||||
cancelButtonIndex: FILE_CANCEL_INDEX
|
|
||||||
}, (actionIndex) => {
|
|
||||||
this.handleMessageBoxActions(actionIndex);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMessageBoxActions = (actionIndex) => {
|
|
||||||
switch (actionIndex) {
|
|
||||||
case FILE_PHOTO_INDEX:
|
|
||||||
this.takePhoto();
|
|
||||||
break;
|
|
||||||
case FILE_VIDEO_INDEX:
|
|
||||||
this.takeVideo();
|
|
||||||
break;
|
|
||||||
case FILE_LIBRARY_INDEX:
|
|
||||||
this.chooseFromLibrary();
|
|
||||||
break;
|
|
||||||
case FILE_DOCUMENT_INDEX:
|
|
||||||
this.chooseFile();
|
|
||||||
break;
|
|
||||||
case CREATE_DISCUSSION_INDEX:
|
|
||||||
this.createDiscussion();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
editCancel = () => {
|
editCancel = () => {
|
||||||
|
@ -672,16 +699,22 @@ class MessageBox extends Component {
|
||||||
|
|
||||||
submit = async() => {
|
submit = async() => {
|
||||||
const {
|
const {
|
||||||
onSubmit, rid: roomId, tmid
|
onSubmit, rid: roomId, tmid, showSend, sharing
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const message = this.text;
|
const message = this.text;
|
||||||
|
|
||||||
|
// if sharing, only execute onSubmit prop
|
||||||
|
if (sharing) {
|
||||||
|
onSubmit(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.clearInput();
|
this.clearInput();
|
||||||
this.debouncedOnChangeText.stop();
|
this.debouncedOnChangeText.stop();
|
||||||
this.closeEmoji();
|
this.closeEmoji();
|
||||||
this.stopTrackingMention();
|
this.stopTrackingMention();
|
||||||
this.handleTyping(false);
|
this.handleTyping(false);
|
||||||
if (message.trim() === '') {
|
if (message.trim() === '' && !showSend) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -802,7 +835,7 @@ class MessageBox extends Component {
|
||||||
recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview
|
recording, showEmojiKeyboard, showSend, mentions, trackingType, commandPreview, showCommandPreview
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const {
|
const {
|
||||||
editing, message, replying, replyCancel, user, getCustomEmoji, theme, Message_AudioRecorderEnabled
|
editing, message, replying, replyCancel, user, getCustomEmoji, theme, Message_AudioRecorderEnabled, children, isActionsEnabled
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const isAndroidTablet = isTablet && isAndroid ? {
|
const isAndroidTablet = isTablet && isAndroid ? {
|
||||||
|
@ -839,6 +872,7 @@ class MessageBox extends Component {
|
||||||
showEmojiKeyboard={showEmojiKeyboard}
|
showEmojiKeyboard={showEmojiKeyboard}
|
||||||
editing={editing}
|
editing={editing}
|
||||||
showMessageBoxActions={this.showMessageBoxActions}
|
showMessageBoxActions={this.showMessageBoxActions}
|
||||||
|
isActionsEnabled={isActionsEnabled}
|
||||||
editCancel={this.editCancel}
|
editCancel={this.editCancel}
|
||||||
openEmoji={this.openEmoji}
|
openEmoji={this.openEmoji}
|
||||||
closeEmoji={this.closeEmoji}
|
closeEmoji={this.closeEmoji}
|
||||||
|
@ -865,17 +899,21 @@ class MessageBox extends Component {
|
||||||
recordAudioMessage={this.recordAudioMessage}
|
recordAudioMessage={this.recordAudioMessage}
|
||||||
recordAudioMessageEnabled={Message_AudioRecorderEnabled}
|
recordAudioMessageEnabled={Message_AudioRecorderEnabled}
|
||||||
showMessageBoxActions={this.showMessageBoxActions}
|
showMessageBoxActions={this.showMessageBoxActions}
|
||||||
|
isActionsEnabled={isActionsEnabled}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
console.count(`${ this.constructor.name }.render calls`);
|
console.count(`${ this.constructor.name }.render calls`);
|
||||||
const { showEmojiKeyboard, file } = this.state;
|
const { showEmojiKeyboard } = this.state;
|
||||||
const { user, baseUrl, theme } = this.props;
|
const {
|
||||||
|
user, baseUrl, theme, iOSScrollBehavior
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<MessageboxContext.Provider
|
<MessageboxContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -897,12 +935,7 @@ class MessageBox extends Component {
|
||||||
requiresSameParentToManageScrollView
|
requiresSameParentToManageScrollView
|
||||||
addBottomView
|
addBottomView
|
||||||
bottomViewColor={themes[theme].messageboxBackground}
|
bottomViewColor={themes[theme].messageboxBackground}
|
||||||
/>
|
iOSScrollBehavior={iOSScrollBehavior}
|
||||||
<UploadModal
|
|
||||||
isVisible={(file && file.isVisible)}
|
|
||||||
file={file}
|
|
||||||
close={() => this.setState({ file: {} })}
|
|
||||||
submit={this.sendMediaMessage}
|
|
||||||
/>
|
/>
|
||||||
</MessageboxContext.Provider>
|
</MessageboxContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -910,6 +943,7 @@ class MessageBox extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
isMasterDetail: state.app.isMasterDetail,
|
||||||
baseUrl: state.server.server,
|
baseUrl: state.server.server,
|
||||||
threadsEnabled: state.settings.Threads_enabled,
|
threadsEnabled: state.settings.Threads_enabled,
|
||||||
user: getUserSelector(state),
|
user: getUserSelector(state),
|
||||||
|
@ -922,4 +956,4 @@ const dispatchToProps = ({
|
||||||
typing: (rid, status) => userTypingAction(rid, status)
|
typing: (rid, status) => userTypingAction(rid, status)
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(MessageBox);
|
export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(withActionSheet(MessageBox));
|
||||||
|
|
|
@ -103,5 +103,8 @@ export default StyleSheet.create({
|
||||||
},
|
},
|
||||||
scrollViewMention: {
|
scrollViewMention: {
|
||||||
maxHeight: SCROLLVIEW_MENTION_HEIGHT
|
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 PropTypes from 'prop-types';
|
||||||
import ActionSheet from 'react-native-action-sheet';
|
|
||||||
|
|
||||||
import RocketChat from '../lib/rocketchat';
|
import RocketChat from '../lib/rocketchat';
|
||||||
import database from '../lib/database';
|
import database from '../lib/database';
|
||||||
import protectedFunction from '../lib/methods/helpers/protectedFunction';
|
import protectedFunction from '../lib/methods/helpers/protectedFunction';
|
||||||
|
import { useActionSheet } from './ActionSheet';
|
||||||
import I18n from '../i18n';
|
import I18n from '../i18n';
|
||||||
import log from '../utils/log';
|
import log from '../utils/log';
|
||||||
|
|
||||||
class MessageErrorActions extends React.Component {
|
const MessageErrorActions = forwardRef(({ tmid }, ref) => {
|
||||||
static propTypes = {
|
const { showActionSheet } = useActionSheet();
|
||||||
actionsHide: PropTypes.func.isRequired,
|
|
||||||
message: PropTypes.object,
|
|
||||||
tmid: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/sort-comp
|
const handleResend = protectedFunction(async(message) => {
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.handleActionPress = this.handleActionPress.bind(this);
|
|
||||||
this.options = [I18n.t('Cancel'), I18n.t('Delete'), I18n.t('Resend')];
|
|
||||||
this.CANCEL_INDEX = 0;
|
|
||||||
this.DELETE_INDEX = 1;
|
|
||||||
this.RESEND_INDEX = 2;
|
|
||||||
setTimeout(() => {
|
|
||||||
this.showActionSheet();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleResend = protectedFunction(async() => {
|
|
||||||
const { message, tmid } = this.props;
|
|
||||||
await RocketChat.resendMessage(message, tmid);
|
await RocketChat.resendMessage(message, tmid);
|
||||||
});
|
});
|
||||||
|
|
||||||
handleDelete = async() => {
|
const handleDelete = async(message) => {
|
||||||
try {
|
try {
|
||||||
const { message, tmid } = this.props;
|
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const deleteBatch = [];
|
const deleteBatch = [];
|
||||||
const msgCollection = db.collections.get('messages');
|
const msgCollection = db.collections.get('messages');
|
||||||
|
@ -49,7 +30,7 @@ class MessageErrorActions extends React.Component {
|
||||||
try {
|
try {
|
||||||
const msg = await msgCollection.find(message.id);
|
const msg = await msgCollection.find(message.id);
|
||||||
deleteBatch.push(msg.prepareDestroyPermanently());
|
deleteBatch.push(msg.prepareDestroyPermanently());
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Do nothing: message not found
|
// Do nothing: message not found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +49,7 @@ class MessageErrorActions extends React.Component {
|
||||||
// If the whole thread was removed, delete the thread
|
// If the whole thread was removed, delete the thread
|
||||||
const thread = await threadCollection.find(tmid);
|
const thread = await threadCollection.find(tmid);
|
||||||
deleteBatch.push(thread.prepareDestroyPermanently());
|
deleteBatch.push(thread.prepareDestroyPermanently());
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Do nothing: thread not found
|
// Do nothing: thread not found
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -78,7 +59,7 @@ class MessageErrorActions extends React.Component {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
// Do nothing: message not found
|
// Do nothing: message not found
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,39 +69,34 @@ class MessageErrorActions extends React.Component {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e);
|
log(e);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
showActionSheet = () => {
|
const showMessageErrorActions = (message) => {
|
||||||
ActionSheet.showActionSheetWithOptions({
|
showActionSheet({
|
||||||
options: this.options,
|
options: [
|
||||||
cancelButtonIndex: this.CANCEL_INDEX,
|
{
|
||||||
destructiveButtonIndex: this.DELETE_INDEX,
|
title: I18n.t('Resend'),
|
||||||
title: I18n.t('Message_actions')
|
icon: 'send',
|
||||||
}, (actionIndex) => {
|
onPress: () => handleResend(message)
|
||||||
this.handleActionPress(actionIndex);
|
},
|
||||||
|
{
|
||||||
|
title: I18n.t('Delete'),
|
||||||
|
icon: 'trash',
|
||||||
|
danger: true,
|
||||||
|
onPress: () => handleDelete(message)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
hasCancel: true
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
handleActionPress = (actionIndex) => {
|
useImperativeHandle(ref, () => ({
|
||||||
const { actionsHide } = this.props;
|
showMessageErrorActions
|
||||||
switch (actionIndex) {
|
}));
|
||||||
case this.RESEND_INDEX:
|
});
|
||||||
this.handleResend();
|
MessageErrorActions.propTypes = {
|
||||||
break;
|
message: PropTypes.object,
|
||||||
case this.DELETE_INDEX:
|
tmid: PropTypes.string
|
||||||
this.handleDelete();
|
};
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
actionsHide();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MessageErrorActions;
|
export default MessageErrorActions;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
View, Text, FlatList, StyleSheet, SafeAreaView
|
View, Text, FlatList, StyleSheet
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Modal from 'react-native-modal';
|
import Modal from 'react-native-modal';
|
||||||
|
@ -12,8 +12,12 @@ import { CustomIcon } from '../lib/Icons';
|
||||||
import sharedStyles from '../views/Styles';
|
import sharedStyles from '../views/Styles';
|
||||||
import { themes } from '../constants/colors';
|
import { themes } from '../constants/colors';
|
||||||
import { withTheme } from '../theme';
|
import { withTheme } from '../theme';
|
||||||
|
import SafeAreaView from './SafeAreaView';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
safeArea: {
|
||||||
|
backgroundColor: 'transparent'
|
||||||
|
},
|
||||||
titleContainer: {
|
titleContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 10
|
paddingVertical: 10
|
||||||
|
@ -95,12 +99,12 @@ const ModalContent = React.memo(({
|
||||||
}) => {
|
}) => {
|
||||||
if (message && message.reactions) {
|
if (message && message.reactions) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1 }}>
|
<SafeAreaView theme={props.theme} style={styles.safeArea}>
|
||||||
<Touchable onPress={onClose}>
|
<Touchable onPress={onClose}>
|
||||||
<View style={styles.titleContainer}>
|
<View style={styles.titleContainer}>
|
||||||
<CustomIcon
|
<CustomIcon
|
||||||
style={[styles.closeButton, { color: themes[props.theme].buttonText }]}
|
style={[styles.closeButton, { color: themes[props.theme].buttonText }]}
|
||||||
name='cross'
|
name='Cross'
|
||||||
size={20}
|
size={20}
|
||||||
/>
|
/>
|
||||||
<Text style={[styles.title, { color: themes[props.theme].buttonText }]}>{I18n.t('Reactions')}</Text>
|
<Text style={[styles.title, { color: themes[props.theme].buttonText }]}>{I18n.t('Reactions')}</Text>
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Image, StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { CustomIcon } from '../lib/Icons';
|
import { CustomIcon } from '../lib/Icons';
|
||||||
import { STATUS_COLORS, themes } from '../constants/colors';
|
import { STATUS_COLORS, themes } from '../constants/colors';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
style: {
|
icon: {
|
||||||
marginRight: 7,
|
marginTop: 3,
|
||||||
marginTop: 3
|
marginRight: 4
|
||||||
},
|
|
||||||
discussion: {
|
|
||||||
marginRight: 6
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -23,22 +20,32 @@ const RoomTypeIcon = React.memo(({
|
||||||
|
|
||||||
const color = themes[theme].auxiliaryText;
|
const color = themes[theme].auxiliaryText;
|
||||||
|
|
||||||
|
let icon = 'lock';
|
||||||
if (type === 'discussion') {
|
if (type === 'discussion') {
|
||||||
// FIXME: These are temporary only. We should have all room icons on <Customicon />, but our design team is still working on this.
|
icon = 'chat';
|
||||||
return <CustomIcon name='chat' size={13} style={[styles.style, styles.iconColor, styles.discussion, { color }]} />;
|
} else if (type === 'c') {
|
||||||
|
icon = 'hash';
|
||||||
|
} else if (type === 'd') {
|
||||||
|
if (isGroupChat) {
|
||||||
|
icon = 'team';
|
||||||
|
} else {
|
||||||
|
icon = 'at';
|
||||||
|
}
|
||||||
|
} else if (type === 'l') {
|
||||||
|
icon = 'livechat';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'c') {
|
return (
|
||||||
return <Image source={{ uri: 'hashtag' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />;
|
<CustomIcon
|
||||||
} if (type === 'd') {
|
name={icon}
|
||||||
if (isGroupChat) {
|
size={size}
|
||||||
return <CustomIcon name='team' size={13} style={[styles.style, styles.discussion, { color }]} />;
|
style={[
|
||||||
}
|
type === 'l' && status ? { color: STATUS_COLORS[status] } : { color },
|
||||||
return <CustomIcon name='at' size={13} style={[styles.style, styles.discussion, { color }]} />;
|
styles.icon,
|
||||||
} if (type === 'l') {
|
style
|
||||||
return <CustomIcon name='omnichannel' size={13} style={[styles.style, styles.discussion, { color: STATUS_COLORS[status] }]} />;
|
]}
|
||||||
}
|
/>
|
||||||
return <Image source={{ uri: 'lock' }} style={[styles.style, style, { width: size, height: size, tintColor: color }]} />;
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
RoomTypeIcon.propTypes = {
|
RoomTypeIcon.propTypes = {
|
||||||
|
@ -51,7 +58,7 @@ RoomTypeIcon.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
RoomTypeIcon.defaultProps = {
|
RoomTypeIcon.defaultProps = {
|
||||||
size: 10
|
size: 16
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RoomTypeIcon;
|
export default RoomTypeIcon;
|
||||||
|
|
|
@ -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 { isIOS } from '../utils/deviceInfo';
|
||||||
import { themes } from '../constants/colors';
|
import { themes } from '../constants/colors';
|
||||||
import { withTheme } from '../theme';
|
|
||||||
|
|
||||||
const StatusBar = React.memo(({ theme }) => {
|
const StatusBar = React.memo(({ theme, barStyle, backgroundColor }) => {
|
||||||
let barStyle = 'light-content';
|
if (!barStyle) {
|
||||||
if (theme === 'light' && isIOS) {
|
barStyle = 'light-content';
|
||||||
barStyle = 'dark-content';
|
if (theme === 'light' && isIOS) {
|
||||||
|
barStyle = 'dark-content';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return <StatusBarRN backgroundColor={themes[theme].headerBackground} barStyle={barStyle} animated />;
|
return <StatusBarRN backgroundColor={backgroundColor ?? themes[theme].headerBackground} barStyle={barStyle} animated />;
|
||||||
});
|
});
|
||||||
|
|
||||||
StatusBar.propTypes = {
|
StatusBar.propTypes = {
|
||||||
theme: PropTypes.string
|
theme: PropTypes.string,
|
||||||
|
barStyle: PropTypes.string,
|
||||||
|
backgroundColor: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withTheme(StatusBar);
|
export default StatusBar;
|
||||||
|
|
|
@ -111,7 +111,7 @@ export default class RCTextInput extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<BorderlessButton onPress={this.tooglePassword} style={[styles.iconContainer, styles.iconRight]}>
|
<BorderlessButton onPress={this.tooglePassword} style={[styles.iconContainer, styles.iconRight]}>
|
||||||
<CustomIcon
|
<CustomIcon
|
||||||
name={showPassword ? 'Eye' : 'eye-off'}
|
name={showPassword ? 'eye' : 'eye-off'}
|
||||||
testID={testID ? `${ testID }-icon-right` : null}
|
testID={testID ? `${ testID }-icon-right` : null}
|
||||||
style={{ color: themes[theme].auxiliaryText }}
|
style={{ color: themes[theme].auxiliaryText }}
|
||||||
size={20}
|
size={20}
|
||||||
|
|
|
@ -5,12 +5,12 @@ import PropTypes from 'prop-types';
|
||||||
import { sha256 } from 'js-sha256';
|
import { sha256 } from 'js-sha256';
|
||||||
import Modal from 'react-native-modal';
|
import Modal from 'react-native-modal';
|
||||||
import useDeepCompareEffect from 'use-deep-compare-effect';
|
import useDeepCompareEffect from 'use-deep-compare-effect';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import TextInput from '../TextInput';
|
import TextInput from '../TextInput';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import EventEmitter from '../../utils/events';
|
import EventEmitter from '../../utils/events';
|
||||||
import { withTheme } from '../../theme';
|
import { withTheme } from '../../theme';
|
||||||
import { withSplit } from '../../split';
|
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import Button from '../Button';
|
import Button from '../Button';
|
||||||
import sharedStyles from '../../views/Styles';
|
import sharedStyles from '../../views/Styles';
|
||||||
|
@ -36,7 +36,7 @@ const methods = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const TwoFactor = React.memo(({ theme, split }) => {
|
const TwoFactor = React.memo(({ theme, isMasterDetail }) => {
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [data, setData] = useState({});
|
const [data, setData] = useState({});
|
||||||
const [code, setCode] = useState('');
|
const [code, setCode] = useState('');
|
||||||
|
@ -93,7 +93,7 @@ const TwoFactor = React.memo(({ theme, split }) => {
|
||||||
hideModalContentWhileAnimating
|
hideModalContentWhileAnimating
|
||||||
>
|
>
|
||||||
<View style={styles.container} testID='two-factor'>
|
<View style={styles.container} testID='two-factor'>
|
||||||
<View style={[styles.content, split && [sharedStyles.modal, sharedStyles.modalFormSheet], { backgroundColor: themes[theme].backgroundColor }]}>
|
<View style={[styles.content, isMasterDetail && [sharedStyles.modalFormSheet, styles.tablet], { backgroundColor: themes[theme].backgroundColor }]}>
|
||||||
<Text style={[styles.title, { color }]}>{I18n.t(method?.title || 'Two_Factor_Authentication')}</Text>
|
<Text style={[styles.title, { color }]}>{I18n.t(method?.title || 'Two_Factor_Authentication')}</Text>
|
||||||
{method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null}
|
{method?.text ? <Text style={[styles.subtitle, { color }]}>{I18n.t(method.text)}</Text> : null}
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -134,7 +134,11 @@ const TwoFactor = React.memo(({ theme, split }) => {
|
||||||
});
|
});
|
||||||
TwoFactor.propTypes = {
|
TwoFactor.propTypes = {
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
split: PropTypes.bool
|
isMasterDetail: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withSplit(withTheme(TwoFactor));
|
const mapStateToProps = state => ({
|
||||||
|
isMasterDetail: state.app.isMasterDetail
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(withTheme(TwoFactor));
|
||||||
|
|
|
@ -37,5 +37,8 @@ export default StyleSheet.create({
|
||||||
buttonContainer: {
|
buttonContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between'
|
justifyContent: 'space-between'
|
||||||
|
},
|
||||||
|
tablet: {
|
||||||
|
height: undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,7 +24,7 @@ const Chip = ({
|
||||||
<>
|
<>
|
||||||
{item.imageUrl ? <FastImage style={styles.chipImage} source={{ uri: item.imageUrl }} /> : null}
|
{item.imageUrl ? <FastImage style={styles.chipImage} source={{ uri: item.imageUrl }} /> : null}
|
||||||
<Text numberOfLines={1} style={[styles.chipText, { color: themes[theme].titleText }]}>{textParser([item.text])}</Text>
|
<Text numberOfLines={1} style={[styles.chipText, { color: themes[theme].titleText }]}>{textParser([item.text])}</Text>
|
||||||
<CustomIcon name='cross' size={16} color={themes[theme].auxiliaryText} />
|
<CustomIcon name='Cross' size={16} color={themes[theme].auxiliaryText} />
|
||||||
</>
|
</>
|
||||||
</Touchable>
|
</Touchable>
|
||||||
);
|
);
|
||||||
|
|
|
@ -22,7 +22,7 @@ const Input = ({
|
||||||
{
|
{
|
||||||
loading
|
loading
|
||||||
? <ActivityIndicator style={[styles.loading, styles.icon]} />
|
? <ActivityIndicator style={[styles.loading, styles.icon]} />
|
||||||
: <CustomIcon name='arrow-down' size={22} color={themes[theme].auxiliaryText} style={styles.icon} />
|
: <CustomIcon name='chevron-down' size={22} color={themes[theme].auxiliaryText} style={styles.icon} />
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
</Touchable>
|
</Touchable>
|
||||||
|
|
|
@ -55,7 +55,7 @@ export const Select = ({
|
||||||
const Icon = () => (
|
const Icon = () => (
|
||||||
loading
|
loading
|
||||||
? <ActivityIndicator style={styles.loading} />
|
? <ActivityIndicator style={styles.loading} />
|
||||||
: <CustomIcon size={22} name='arrow-down' style={isAndroid && styles.icon} color={themes[theme].auxiliaryText} />
|
: <CustomIcon size={22} name='chevron-down' style={isAndroid && styles.icon} color={themes[theme].auxiliaryText} />
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {
|
||||||
View, StyleSheet, Text, Easing, Dimensions
|
View, StyleSheet, Text, Easing
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Audio } from 'expo-av';
|
import { Audio } from 'expo-av';
|
||||||
import Slider from '@react-native-community/slider';
|
import Slider from '@react-native-community/slider';
|
||||||
|
@ -15,9 +15,9 @@ import { CustomIcon } from '../../lib/Icons';
|
||||||
import sharedStyles from '../../views/Styles';
|
import sharedStyles from '../../views/Styles';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import { isAndroid, isIOS } from '../../utils/deviceInfo';
|
import { isAndroid, isIOS } from '../../utils/deviceInfo';
|
||||||
import { withSplit } from '../../split';
|
|
||||||
import MessageContext from './Context';
|
import MessageContext from './Context';
|
||||||
import ActivityIndicator from '../ActivityIndicator';
|
import ActivityIndicator from '../ActivityIndicator';
|
||||||
|
import { withDimensions } from '../../dimensions';
|
||||||
|
|
||||||
const mode = {
|
const mode = {
|
||||||
allowsRecordingIOS: false,
|
allowsRecordingIOS: false,
|
||||||
|
@ -98,8 +98,8 @@ class MessageAudio extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
file: PropTypes.object.isRequired,
|
file: PropTypes.object.isRequired,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
split: PropTypes.bool,
|
getCustomEmoji: PropTypes.func,
|
||||||
getCustomEmoji: PropTypes.func
|
scale: PropTypes.number
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -138,7 +138,7 @@ class MessageAudio extends React.Component {
|
||||||
const {
|
const {
|
||||||
currentTime, duration, paused, loading
|
currentTime, duration, paused, loading
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { file, split, theme } = this.props;
|
const { file, theme } = this.props;
|
||||||
if (nextProps.theme !== theme) {
|
if (nextProps.theme !== theme) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -154,9 +154,6 @@ class MessageAudio extends React.Component {
|
||||||
if (!equal(nextProps.file, file)) {
|
if (!equal(nextProps.file, file)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (nextProps.split !== split) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (nextState.loading !== loading) {
|
if (nextState.loading !== loading) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -249,7 +246,7 @@ class MessageAudio extends React.Component {
|
||||||
loading, paused, currentTime, duration
|
loading, paused, currentTime, duration
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const {
|
const {
|
||||||
file, getCustomEmoji, split, theme
|
file, getCustomEmoji, theme, scale
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { description } = file;
|
const { description } = file;
|
||||||
const { baseUrl, user } = this.context;
|
const { baseUrl, user } = this.context;
|
||||||
|
@ -263,8 +260,7 @@ class MessageAudio extends React.Component {
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.audioContainer,
|
styles.audioContainer,
|
||||||
{ backgroundColor: themes[theme].chatComponentBackground, borderColor: themes[theme].borderColor },
|
{ backgroundColor: themes[theme].chatComponentBackground, borderColor: themes[theme].borderColor }
|
||||||
split && sharedStyles.tabletContent
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Button loading={loading} paused={paused} onPress={this.togglePlayPause} theme={theme} />
|
<Button loading={loading} paused={paused} onPress={this.togglePlayPause} theme={theme} />
|
||||||
|
@ -279,7 +275,7 @@ class MessageAudio extends React.Component {
|
||||||
minimumTrackTintColor={themes[theme].tintColor}
|
minimumTrackTintColor={themes[theme].tintColor}
|
||||||
maximumTrackTintColor={themes[theme].auxiliaryText}
|
maximumTrackTintColor={themes[theme].auxiliaryText}
|
||||||
onValueChange={this.onValueChange}
|
onValueChange={this.onValueChange}
|
||||||
thumbImage={isIOS && { uri: 'audio_thumb', scale: Dimensions.get('window').scale }}
|
thumbImage={isIOS && { uri: 'audio_thumb', scale }}
|
||||||
/>
|
/>
|
||||||
<Text style={[styles.duration, { color: themes[theme].auxiliaryText }]}>{this.duration}</Text>
|
<Text style={[styles.duration, { color: themes[theme].auxiliaryText }]}>{this.duration}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
@ -289,4 +285,4 @@ class MessageAudio extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withSplit(MessageAudio);
|
export default withDimensions(MessageAudio);
|
||||||
|
|
|
@ -22,7 +22,7 @@ const CallButton = React.memo(({
|
||||||
hitSlop={BUTTON_HIT_SLOP}
|
hitSlop={BUTTON_HIT_SLOP}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<CustomIcon name='video' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} />
|
<CustomIcon name='video-1' size={20} style={styles.buttonIcon} color={themes[theme].buttonText} />
|
||||||
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{I18n.t('Click_to_join')}</Text>
|
<Text style={[styles.buttonText, { color: themes[theme].buttonText }]}>{I18n.t('Click_to_join')}</Text>
|
||||||
</>
|
</>
|
||||||
</Touchable>
|
</Touchable>
|
||||||
|
|
|
@ -10,19 +10,17 @@ import Touchable from './Touchable';
|
||||||
import Markdown from '../markdown';
|
import Markdown from '../markdown';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import { formatAttachmentUrl } from '../../lib/utils';
|
import { formatAttachmentUrl } from '../../lib/utils';
|
||||||
import { withSplit } from '../../split';
|
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../../constants/colors';
|
||||||
import sharedStyles from '../../views/Styles';
|
|
||||||
import MessageContext from './Context';
|
import MessageContext from './Context';
|
||||||
|
|
||||||
const ImageProgress = createImageProgress(FastImage);
|
const ImageProgress = createImageProgress(FastImage);
|
||||||
|
|
||||||
const Button = React.memo(({
|
const Button = React.memo(({
|
||||||
children, onPress, split, theme
|
children, onPress, theme
|
||||||
}) => (
|
}) => (
|
||||||
<Touchable
|
<Touchable
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
style={[styles.imageContainer, split && sharedStyles.tabletContent]}
|
style={styles.imageContainer}
|
||||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -42,7 +40,7 @@ export const MessageImage = React.memo(({ img, theme }) => (
|
||||||
));
|
));
|
||||||
|
|
||||||
const ImageContainer = React.memo(({
|
const ImageContainer = React.memo(({
|
||||||
file, imageUrl, showAttachment, getCustomEmoji, split, theme
|
file, imageUrl, showAttachment, getCustomEmoji, theme
|
||||||
}) => {
|
}) => {
|
||||||
const { baseUrl, user } = useContext(MessageContext);
|
const { baseUrl, user } = useContext(MessageContext);
|
||||||
const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
|
const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
|
||||||
|
@ -54,7 +52,7 @@ const ImageContainer = React.memo(({
|
||||||
|
|
||||||
if (file.description) {
|
if (file.description) {
|
||||||
return (
|
return (
|
||||||
<Button split={split} theme={theme} onPress={onPress}>
|
<Button theme={theme} onPress={onPress}>
|
||||||
<View>
|
<View>
|
||||||
<MessageImage img={img} theme={theme} />
|
<MessageImage img={img} theme={theme} />
|
||||||
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
|
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
|
||||||
|
@ -64,19 +62,18 @@ const ImageContainer = React.memo(({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button split={split} theme={theme} onPress={onPress}>
|
<Button theme={theme} onPress={onPress}>
|
||||||
<MessageImage img={img} theme={theme} />
|
<MessageImage img={img} theme={theme} />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}, (prevProps, nextProps) => equal(prevProps.file, nextProps.file) && prevProps.split === nextProps.split && prevProps.theme === nextProps.theme);
|
}, (prevProps, nextProps) => equal(prevProps.file, nextProps.file) && prevProps.theme === nextProps.theme);
|
||||||
|
|
||||||
ImageContainer.propTypes = {
|
ImageContainer.propTypes = {
|
||||||
file: PropTypes.object,
|
file: PropTypes.object,
|
||||||
imageUrl: PropTypes.string,
|
imageUrl: PropTypes.string,
|
||||||
showAttachment: PropTypes.func,
|
showAttachment: PropTypes.func,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
getCustomEmoji: PropTypes.func,
|
getCustomEmoji: PropTypes.func
|
||||||
split: PropTypes.bool
|
|
||||||
};
|
};
|
||||||
ImageContainer.displayName = 'MessageImageContainer';
|
ImageContainer.displayName = 'MessageImageContainer';
|
||||||
|
|
||||||
|
@ -89,9 +86,8 @@ ImageContainer.displayName = 'MessageImage';
|
||||||
Button.propTypes = {
|
Button.propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
onPress: PropTypes.func,
|
onPress: PropTypes.func,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string
|
||||||
split: PropTypes.bool
|
|
||||||
};
|
};
|
||||||
ImageContainer.displayName = 'MessageButton';
|
ImageContainer.displayName = 'MessageButton';
|
||||||
|
|
||||||
export default withSplit(ImageContainer);
|
export default ImageContainer;
|
||||||
|
|