forked from Ivasoft/mattermost-mobile
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93265b3de0 | ||
|
|
0d70372a3c | ||
|
|
72087391dc | ||
|
|
9c89fe2907 | ||
|
|
9684328123 | ||
|
|
c3ef5e6f38 | ||
|
|
e36f63c84d | ||
|
|
ce05b9c98b | ||
|
|
388294a124 | ||
|
|
1a12abfe50 | ||
|
|
0210d6e1eb | ||
|
|
49bcf185e6 | ||
|
|
61ecf7d159 | ||
|
|
ead5f2860f | ||
|
|
09ac903630 | ||
|
|
a471379cb2 | ||
|
|
eaf128b2a0 | ||
|
|
96f5cd2c11 | ||
|
|
6b23c230ed | ||
|
|
63a3e4eb89 |
48
.flowconfig
48
.flowconfig
@@ -1,48 +1,58 @@
|
||||
[ignore]
|
||||
; We fork some components by platform
|
||||
|
||||
# We fork some components by platform.
|
||||
.*/*[.]android.js
|
||||
|
||||
; Ignore "BUCK" generated dirs
|
||||
# Ignore templates with `@flow` in header
|
||||
.*/local-cli/generator.*
|
||||
|
||||
# Ignore malformed json
|
||||
.*/node_modules/y18n/test/.*\.json
|
||||
|
||||
# Ignore the website subdir
|
||||
<PROJECT_ROOT>/website/.*
|
||||
|
||||
# Ignore BUCK generated dirs
|
||||
<PROJECT_ROOT>/\.buckd/
|
||||
|
||||
; Ignore unexpected extra "@providesModule"
|
||||
.*/node_modules/.*/node_modules/fbjs/.*
|
||||
# Ignore unexpected extra @providesModule
|
||||
.*/node_modules/commoner/test/source/widget/share.js
|
||||
|
||||
; Ignore duplicate module providers
|
||||
; For RN Apps installed via npm, "Libraries" folder is inside
|
||||
; "node_modules/react-native" but in the source repo it is in the root
|
||||
# Ignore duplicate module providers
|
||||
# For RN Apps installed via npm, "Libraries" folder is inside node_modules/react-native but in the source repo it is in the root
|
||||
.*/Libraries/react-native/React.js
|
||||
|
||||
; Ignore polyfills
|
||||
.*/Libraries/polyfills/.*
|
||||
.*/Libraries/react-native/ReactNative.js
|
||||
.*/node_modules/jest-runtime/build/__tests__/.*
|
||||
|
||||
[include]
|
||||
|
||||
[libs]
|
||||
node_modules/react-native/Libraries/react-native/react-native-interface.js
|
||||
node_modules/react-native/flow/
|
||||
node_modules/react-native/flow
|
||||
flow/
|
||||
|
||||
[options]
|
||||
emoji=true
|
||||
|
||||
module.system=haste
|
||||
|
||||
esproposal.class_static_fields=enable
|
||||
esproposal.class_instance_fields=enable
|
||||
|
||||
experimental.strict_type_args=true
|
||||
|
||||
munge_underscores=true
|
||||
|
||||
module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub'
|
||||
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
|
||||
|
||||
suppress_type=$FlowIssue
|
||||
suppress_type=$FlowFixMe
|
||||
suppress_type=$FlowFixMeProps
|
||||
suppress_type=$FlowFixMeState
|
||||
suppress_type=$FixMe
|
||||
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-2]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-2]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
|
||||
|
||||
unsafe.enable_getters_and_setters=true
|
||||
|
||||
[version]
|
||||
^0.56.0
|
||||
^0.32.0
|
||||
|
||||
30
.gitignore
vendored
30
.gitignore
vendored
@@ -1,8 +1,5 @@
|
||||
assets/override
|
||||
dist
|
||||
*.zip
|
||||
server.PID
|
||||
mattermost.keystore
|
||||
|
||||
# OSX
|
||||
#
|
||||
@@ -29,27 +26,28 @@ DerivedData
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
|
||||
# Android/IntelliJ
|
||||
# Android/IJ
|
||||
#
|
||||
build/
|
||||
*.iml
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
|
||||
# node.js
|
||||
#
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
.npminstall
|
||||
yarn-error.log
|
||||
|
||||
# yarn
|
||||
#
|
||||
.yarninstall
|
||||
|
||||
# BUCK
|
||||
buck-out/
|
||||
\.buckd/
|
||||
android/app/libs
|
||||
*.keystore
|
||||
android/keystores/debug.keystore
|
||||
|
||||
# Vim
|
||||
[._]*.s[a-w][a-z]
|
||||
@@ -60,17 +58,10 @@ Session.vim
|
||||
*~
|
||||
tags
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
*/fastlane/screenshots
|
||||
|
||||
*.zip
|
||||
server.PID
|
||||
mattermost.keystore
|
||||
|
||||
# Sentry
|
||||
android/sentry.properties
|
||||
@@ -82,6 +73,3 @@ ios/sentry.properties
|
||||
# Pods
|
||||
.podinstall
|
||||
ios/Pods/
|
||||
|
||||
#editor-settings
|
||||
.vscode
|
||||
83
CHANGELOG.md
83
CHANGELOG.md
@@ -1,88 +1,5 @@
|
||||
# Mattermost Mobile Apps Changelog
|
||||
|
||||
## v1.5.2 Release
|
||||
- Release Date: January 12, 2018
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue causing some Android devices to crash on launch
|
||||
- Fixed an issue with the app occasionally crashing when receiving push notifications in a new channel
|
||||
- Channel footer area is now refreshed when switching between Group and Direct Message channels
|
||||
- Fixed an issue on some Android devices so Mattermost verifies it has permissions to access ringtones
|
||||
- Fixed an issue where the text box overlapped the keyboard on some iOS devices using multiple keyboard layouts
|
||||
- Fixed an issue with video uploads on Android devices
|
||||
- Fixed an issue with GIF uploads on iOS devices
|
||||
- Fixed an issue with the mention badge flickering on the channel drawer icon when there were over 10 unread mentions
|
||||
- Fixed an issue with the app occasionally freezing when requesting the RefreshToken
|
||||
|
||||
## v1.5.1 Release
|
||||
|
||||
- Release Date: December 7, 2017
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue with the upgrade app screen showing with a transparent background
|
||||
- Fixed an issue with clearing or replying to notifications sometimes crashing the app on Android
|
||||
- Fixed an issue with the app sometimes crashing due to a missing function in the swiping control
|
||||
|
||||
## v1.5 Release
|
||||
|
||||
- Release Date: December 6, 2017
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Highlights
|
||||
|
||||
#### File Viewer
|
||||
- Preview videos, RTF, PDFs, Word, Excel, and Powerpoint files
|
||||
|
||||
#### iPhone X Compatibility
|
||||
- Added support for iPhone X
|
||||
|
||||
#### Slash Commands
|
||||
- Added support for using custom slash commands
|
||||
- Added support for built-in slash commands /away, /online, /offline, /dnd, /header, /purpose, /kick, /me, /shrug
|
||||
|
||||
### Improvements
|
||||
- In iOS, 3D touch can now be used to peek into a channel to view the contents, and quickly mark it as read
|
||||
- Markdown images in posts now render
|
||||
- Copy posts, URLs, and code blocks
|
||||
- Opening a channel with Unread messages takes you to the "New Messages" indicator
|
||||
- Support for data retention, interactive message buttons, and viewing Do Not Disturb statuses depending on the server version
|
||||
- (Edited) indicator now shows up beside edited posts
|
||||
- Added a "Recently Used" section for emoji reactions
|
||||
|
||||
### Bug Fixes
|
||||
- Android notifications now follow the default system setting for vibration
|
||||
- Fixed app crashing when opening notification settings on Android
|
||||
- Fixed an issue where the "Proceed" button on sign in screen stopped working after pressing logout multiple times
|
||||
- HEIC images posted from iPhones now get converted to JPEG before uploading
|
||||
|
||||
## v1.4.1 Release
|
||||
|
||||
Release Date: Nov 15, 2017
|
||||
Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed network detection issue causing some people to be unable to access the app
|
||||
- Fixed issue with lag when pressing send button
|
||||
- Fixed app crash when opening notification settings
|
||||
- Fixed various other bugs to reduce app crashes
|
||||
|
||||
## v1.4 Release
|
||||
|
||||
- Release Date: November 6, 2017
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Highlights
|
||||
|
||||
#### Performance improvements
|
||||
- Various performance improvements to decrease channel load times
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed issue with Android app sometimes showing a white screen when re-opening the app
|
||||
- Fixed an issue with orientation lock not working on Android
|
||||
|
||||
## v1.3 Release
|
||||
|
||||
- Release Date: October 5, 2017
|
||||
|
||||
14
Jenkinsfile
vendored
14
Jenkinsfile
vendored
@@ -1,14 +0,0 @@
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
stages {
|
||||
stage('Test') {
|
||||
steps {
|
||||
echo 'assets/base/config.json'
|
||||
sh 'cat assets/base/config.json'
|
||||
sh 'touch .podinstall'
|
||||
sh 'make test || exit 1'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
239
Makefile
239
Makefile
@@ -1,14 +1,11 @@
|
||||
.PHONY: pre-run clean
|
||||
.PHONY: check-style
|
||||
.PHONY: start stop
|
||||
.PHONY: run run-ios run-android
|
||||
.PHONY: build-ios build-android unsigned-ios unsigned-android
|
||||
.PHONY: test help
|
||||
.PHONY: run run-ios run-android check-style test clean post-install start stop
|
||||
.PHONY: check-ios-target build-ios
|
||||
.PHONY: check-android-target prepare-android-build build-android
|
||||
.PHONY: start-packager stop-packager
|
||||
|
||||
ios_target := $(filter-out build-ios,$(MAKECMDGOALS))
|
||||
android_target := $(filter-out build-android,$(MAKECMDGOALS))
|
||||
POD := $(shell command -v pod 2> /dev/null)
|
||||
OS := $(shell sh -c 'uname -s 2>/dev/null')
|
||||
BASE_ASSETS = $(shell find assets/base -type d) $(shell find assets/base -type f -name '*')
|
||||
OVERRIDE_ASSETS = $(shell find assets/override -type d 2> /dev/null) $(shell find assets/override -type f -name '*' 2> /dev/null)
|
||||
|
||||
.yarninstall: package.json
|
||||
@if ! [ $(shell command -v yarn 2> /dev/null) ]; then \
|
||||
@@ -22,7 +19,6 @@ OVERRIDE_ASSETS = $(shell find assets/override -type d 2> /dev/null) $(shell fin
|
||||
@touch $@
|
||||
|
||||
.podinstall:
|
||||
ifeq ($(OS), Darwin)
|
||||
ifdef POD
|
||||
@echo Getting Cocoapods dependencies;
|
||||
@cd ios && pod install;
|
||||
@@ -30,10 +26,13 @@ else
|
||||
@echo "Cocoapods is not installed https://cocoapods.org/"
|
||||
@exit 1
|
||||
endif
|
||||
endif
|
||||
|
||||
@touch $@
|
||||
|
||||
BASE_ASSETS = $(shell find assets/base -type d) $(shell find assets/base -type f -name '*')
|
||||
OVERRIDE_ASSETS = $(shell find assets/override -type d 2> /dev/null) $(shell find assets/override -type f -name '*' 2> /dev/null)
|
||||
dist/assets: $(BASE_ASSETS) $(OVERRIDE_ASSETS)
|
||||
|
||||
@mkdir -p dist
|
||||
|
||||
@if [ -e dist/assets ] ; then \
|
||||
@@ -43,14 +42,60 @@ dist/assets: $(BASE_ASSETS) $(OVERRIDE_ASSETS)
|
||||
@echo "Generating app assets"
|
||||
@node scripts/make-dist-assets.js
|
||||
|
||||
pre-run: | .yarninstall .podinstall dist/assets ## Installs dependencies and assets
|
||||
pre-run: | .yarninstall .podinstall dist/assets
|
||||
|
||||
check-style: .yarninstall ## Runs eslint
|
||||
run: run-ios
|
||||
|
||||
start: | pre-run start-packager
|
||||
|
||||
stop: stop-packager
|
||||
|
||||
check-device-ios:
|
||||
@if ! [ $(shell command -v xcodebuild) ]; then \
|
||||
@echo "xcode is not installed"; \
|
||||
@exit 1; \
|
||||
fi
|
||||
@if ! [ $(shell command -v watchman) ]; then \
|
||||
@echo "watchman is not installed"; \
|
||||
@exit 1; \
|
||||
fi
|
||||
|
||||
run-ios: | check-device-ios start
|
||||
@echo Running iOS app in development
|
||||
@react-native run-ios --simulator="${SIMULATOR}"
|
||||
|
||||
check-device-android:
|
||||
@if ! [ $(ANDROID_HOME) ]; then \
|
||||
@echo "ANDROID_HOME is not set"; \
|
||||
@exit 1; \
|
||||
fi
|
||||
@if ! [ $(shell command -v adb 2> /dev/null) ]; then \
|
||||
@echo "adb is not installed"; \
|
||||
@exit 1; \
|
||||
fi
|
||||
ifneq ($(shell adb get-state),device)
|
||||
@echo "no android device or emulator is running"
|
||||
@exit 1;
|
||||
endif
|
||||
@if ! [ $(shell command -v watchman 2> /dev/null) ]; then \
|
||||
@echo "watchman is not installed"; \
|
||||
@exit 1; \
|
||||
fi
|
||||
|
||||
run-android: | check-device-android start prepare-android-build
|
||||
@echo Running Android app in development
|
||||
@react-native run-android --no-packager
|
||||
|
||||
test: | pre-run check-style
|
||||
@yarn test
|
||||
|
||||
check-style: .yarninstall
|
||||
@echo Checking for style guide compliance
|
||||
@node_modules/.bin/eslint --ext \".js\" --ignore-pattern node_modules --quiet .
|
||||
|
||||
clean: ## Cleans dependencies, previous builds and temp files
|
||||
clean:
|
||||
@echo Cleaning started
|
||||
|
||||
@yarn cache clean
|
||||
@rm -rf node_modules
|
||||
@rm -f .yarninstall
|
||||
@@ -59,6 +104,7 @@ clean: ## Cleans dependencies, previous builds and temp files
|
||||
@rm -rf ios/build
|
||||
@rm -rf ios/Pods
|
||||
@rm -rf android/app/build
|
||||
|
||||
@echo Cleanup finished
|
||||
|
||||
post-install:
|
||||
@@ -75,152 +121,95 @@ post-install:
|
||||
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-relativeformat/package.json
|
||||
@sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
|
||||
@sed -i'' -e 's|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_FULL_USER);|g' node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/params/Orientation.java
|
||||
@sed -i'' -e "s|var AndroidTextInput = requireNativeComponent('AndroidTextInput', null);|var AndroidTextInput = requireNativeComponent('CustomTextInput', null);|g" node_modules/react-native/Libraries/Components/TextInput/TextInput.js
|
||||
@if [ $(shell grep "const Platform" node_modules/react-native/Libraries/Lists/VirtualizedList.js | grep -civ grep) -eq 0 ]; then \
|
||||
sed $ -i'' -e "s|const ReactNative = require('ReactNative');|const ReactNative = require('ReactNative');`echo $\\\\\\r;`const Platform = require('Platform');|g" node_modules/react-native/Libraries/Lists/VirtualizedList.js; \
|
||||
fi
|
||||
@sed -i'' -e 's|transform: \[{scaleY: -1}\],|...Platform.select({android: {transform: \[{perspective: 1}, {scaleY: -1}\]}, ios: {transform: \[{scaleY: -1}\]}}),|g' node_modules/react-native/Libraries/Lists/VirtualizedList.js
|
||||
@cd ./node_modules/react-native-svg/ios && rm -rf PerformanceBezier QuartzBookPack && yarn run postinstall
|
||||
@cd ./node_modules/react-native-svg/ios && rm -rf PerformanceBezier && git clone https://github.com/adamwulf/PerformanceBezier.git
|
||||
@cd ./node_modules/mattermost-redux && yarn run build
|
||||
|
||||
start: | pre-run ## Starts the React Native packager server
|
||||
start-packager:
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
node ./node_modules/react-native/local-cli/cli.js start; \
|
||||
node ./node_modules/react-native/local-cli/cli.js start --reset-cache & echo $$! > server.PID; \
|
||||
else \
|
||||
echo React Native packager server already running; \
|
||||
ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' > server.PID; \
|
||||
ps -e | grep -i "cli.js start" | grep -v grep | awk '{print $$1}' > server.PID; \
|
||||
fi
|
||||
|
||||
stop: ## Stops the React Native packager server
|
||||
stop-packager:
|
||||
@echo Stopping React Native packager server
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 1 ]; then \
|
||||
ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9; \
|
||||
@if [ -e "server.PID" ] ; then \
|
||||
kill -9 `cat server.PID` && rm server.PID; \
|
||||
echo React Native packager server stopped; \
|
||||
else \
|
||||
echo No React Native packager server running; \
|
||||
fi
|
||||
|
||||
check-device-ios:
|
||||
@if ! [ $(shell command -v xcodebuild) ]; then \
|
||||
echo "xcode is not installed"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if ! [ $(shell command -v watchman) ]; then \
|
||||
echo "watchman is not installed"; \
|
||||
exit 1; \
|
||||
fi
|
||||
check-ios-target:
|
||||
ifeq ($(ios_target), )
|
||||
@echo No target set to build iOS app
|
||||
@echo "Try running make build-ios TARGET where TARGET is one of dev, beta or release"
|
||||
@exit 1
|
||||
endif
|
||||
ifneq ($(ios_target), $(filter $(ios_target),dev beta release))
|
||||
@echo Invalid target set to build iOS app
|
||||
@echo "Try running make build-ios TARGET where TARGET is one of dev, beta or release"
|
||||
@exit 1
|
||||
endif
|
||||
|
||||
check-device-android:
|
||||
@if ! [ $(ANDROID_HOME) ]; then \
|
||||
@echo "ANDROID_HOME is not set"; \
|
||||
@exit 1; \
|
||||
fi
|
||||
@if ! [ $(shell command -v adb 2> /dev/null) ]; then \
|
||||
@echo "adb is not installed"; \
|
||||
@exit 1; \
|
||||
fi
|
||||
do-build-ios:
|
||||
@echo "Building ios $(ios_target) app"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios $(ios_target)
|
||||
|
||||
@echo "Connect your Android device or open the emulator"
|
||||
@adb wait-for-device
|
||||
|
||||
@if ! [ $(shell command -v watchman 2> /dev/null) ]; then \
|
||||
@echo "watchman is not installed"; \
|
||||
@exit 1; \
|
||||
fi
|
||||
build-ios: | check-ios-target pre-run check-style start-packager do-build-ios stop-packager
|
||||
|
||||
check-android-target:
|
||||
ifeq ($(android_target), )
|
||||
@echo No target set to build Android app
|
||||
@echo "Try running make build-android TARGET where TARGET is one of dev, beta or release"
|
||||
@exit 1
|
||||
endif
|
||||
ifneq ($(android_target), $(filter $(android_target),dev alpha release))
|
||||
@echo Invalid target set to build Android app
|
||||
@echo "Try running make build-android TARGET where TARGET is one of dev, beta or release"
|
||||
@exit 1
|
||||
endif
|
||||
|
||||
prepare-android-build:
|
||||
@rm -rf ./node_modules/react-native/local-cli/templates/HelloWorld
|
||||
@rm -rf ./node_modules/react-native-linear-gradient/Examples/
|
||||
@rm -rf ./node_modules/react-native-orientation/demo/
|
||||
|
||||
run: run-ios ## alias for run-ios
|
||||
do-build-android:
|
||||
@echo "Building android $(android_target) app"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android $(android_target)
|
||||
|
||||
run-ios: | check-device-ios pre-run ## Runs the app on an iOS simulator
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
node ./node_modules/react-native/local-cli/cli.js start & echo Running iOS app in development; \
|
||||
react-native run-ios --simulator="${SIMULATOR}"; \
|
||||
wait; \
|
||||
else \
|
||||
echo Running iOS app in development; \
|
||||
react-native run-ios --simulator="${SIMULATOR}"; \
|
||||
fi
|
||||
build-android: | check-android-target pre-run check-style start-packager prepare-android-build do-build-android stop-packager
|
||||
|
||||
run-android: | check-device-android pre-run prepare-android-build ## Runs the app on an Android emulator or dev device
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
node ./node_modules/react-native/local-cli/cli.js start & echo Running Android app in development; \
|
||||
if [ ! -z ${VARIANT} ]; then \
|
||||
react-native run-android --no-packager --variant=${VARIANT}; \
|
||||
else \
|
||||
react-native run-android --no-packager; \
|
||||
fi; \
|
||||
wait; \
|
||||
else \
|
||||
echo Running Android app in development; \
|
||||
if [ ! -z ${VARIANT} ]; then \
|
||||
react-native run-android --no-packager --variant=${VARIANT}; \
|
||||
else \
|
||||
react-native run-android --no-packager; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
build-ios: | pre-run check-style ## Creates an iOS build
|
||||
ifneq ($(IOS_APP_GROUP),)
|
||||
@mkdir -p assets/override
|
||||
@echo "{\n\t\"AppGroupId\": \"$$IOS_APP_GROUP\"\n}" > assets/override/config.json
|
||||
endif
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
node ./node_modules/react-native/local-cli/cli.js start & echo; \
|
||||
fi
|
||||
@echo "Building iOS app"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios build
|
||||
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
|
||||
@rm -rf assets/override
|
||||
|
||||
build-android: | pre-run check-style prepare-android-build ## Creates an Android build
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
node ./node_modules/react-native/local-cli/cli.js start & echo; \
|
||||
fi
|
||||
@echo "Building Android app"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android build
|
||||
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
|
||||
|
||||
unsigned-ios: pre-run check-style
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
node ./node_modules/react-native/local-cli/cli.js start & echo; \
|
||||
fi
|
||||
do-unsigned-ios:
|
||||
@echo "Building unsigned iOS app"
|
||||
ifneq ($(IOS_APP_GROUP),)
|
||||
@mkdir -p assets/override
|
||||
@echo "{\n\t\"AppGroupId\": \"$$IOS_APP_GROUP\"\n}" > assets/override/config.json
|
||||
endif
|
||||
@cd fastlane && NODE_ENV=production bundle exec fastlane ios unsigned
|
||||
@mkdir -p build-ios
|
||||
@cd ios/ && xcodebuild -workspace Mattermost.xcworkspace/ -scheme Mattermost -sdk iphoneos -configuration Relase -parallelizeTargets -resultBundlePath ../build-ios/result -derivedDataPath ../build-ios/ CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
|
||||
@cd build-ios/ && mkdir -p Payload && cp -R Build/Products/Release-iphoneos/Mattermost.app Payload/ && zip -r Mattermost-unsigned.ipa Payload/
|
||||
@mv build-ios/Mattermost-unsigned.ipa .
|
||||
@rm -rf build-ios/
|
||||
@rm -rf assets/override
|
||||
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
|
||||
|
||||
unsigned-android: pre-run check-style prepare-android-build
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
node ./node_modules/react-native/local-cli/cli.js start & echo; \
|
||||
fi
|
||||
do-unsigned-android:
|
||||
@echo "Building unsigned Android app"
|
||||
@cd fastlane && NODE_ENV=production bundle exec fastlane android unsigned
|
||||
@mv android/app/build/outputs/apk/app-unsigned-unsigned.apk ./Mattermost-unsigned.apk
|
||||
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
|
||||
|
||||
test: | pre-run check-style ## Runs tests
|
||||
@yarn test
|
||||
unsigned-android: pre-run check-style start-packager do-unsigned-android stop-packager
|
||||
|
||||
## Help documentation https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
||||
help:
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
unsigned-ios: pre-run check-style start-packager do-unsigned-ios stop-packager
|
||||
|
||||
alpha:
|
||||
@:
|
||||
|
||||
dev:
|
||||
@:
|
||||
|
||||
beta:
|
||||
@:
|
||||
|
||||
release:
|
||||
@:
|
||||
|
||||
1277
NOTICE.txt
1277
NOTICE.txt
File diff suppressed because it is too large
Load Diff
90
README.md
90
README.md
@@ -5,9 +5,9 @@
|
||||
**Supported iOS versions:** 9.3+
|
||||
**Supported Android versions:** 5.0+
|
||||
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 11 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 11 languages. Learn more at https://mattermost.com.
|
||||
|
||||
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or build them yourself.
|
||||
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or package them yourself.
|
||||
|
||||
We plan on releasing monthly updates with new features - check the [changelog](https://github.com/mattermost/mattermost-mobile/blob/master/CHANGELOG.md) for what features are currently supported!
|
||||
|
||||
@@ -36,7 +36,85 @@ To help with testing app updates before they're released, you can:
|
||||
3. Follow [these instructions](https://docs.mattermost.com/developer/mobile-developer-setup.html) to set up your developer environment
|
||||
4. Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) on our team site to ask questions
|
||||
|
||||
# Installing Dependencies
|
||||
|
||||
Follow the [React Native Getting Started Guide](https://facebook.github.io/react-native/docs/getting-started.html) for detailed instructions on setting up your local machine for development.
|
||||
|
||||
# Detailed configuration:
|
||||
|
||||
## Mac
|
||||
|
||||
- General requirements
|
||||
|
||||
- XCode 8.3
|
||||
- Install required packages using homebrew:
|
||||
```bash
|
||||
$ brew install watchman
|
||||
$ brew install yarn
|
||||
```
|
||||
|
||||
- Clone repository and configure:
|
||||
```bash
|
||||
$ git clone git@github.com:mattermost/mattermost-mobile.git
|
||||
$ cd mattermost-mobile
|
||||
$ npm install
|
||||
$ npm install -g react-native-cli
|
||||
```
|
||||
|
||||
- Run application
|
||||
```bash
|
||||
$ make run
|
||||
```
|
||||
|
||||
- Stop the packager server
|
||||
```bash
|
||||
$ make stop
|
||||
```
|
||||
## Linux:
|
||||
|
||||
- General requiriments:
|
||||
|
||||
- JDK 7 or greater
|
||||
- Android SDK
|
||||
- Virtualbox
|
||||
- An Android emulator: Genymotion or Android emulator. If using genymotion ensure that it uses existing adb tools (Settings: "Use custom Android SDK Tools")
|
||||
- Install watchman (do this globally):
|
||||
```bash
|
||||
$ git clone https://github.com/facebook/watchman.git
|
||||
$ cd watchman
|
||||
$ git checkout master
|
||||
$ ./autogen.sh
|
||||
$ ./configure
|
||||
$ make
|
||||
$ sudo make install
|
||||
```
|
||||
Configure your kernel to accept a lot of file watches, using a command like:
|
||||
```bash
|
||||
$ sudo sysctl -w fs.inotify.max_user_watches=1048576
|
||||
```
|
||||
|
||||
- Clone repository and configure:
|
||||
```bash
|
||||
$ git clone git@github.com:mattermost/mattermost-mobile.git
|
||||
$ cd mattermost-mobile
|
||||
$ npm install
|
||||
$ npm install -g react-native-cli
|
||||
```
|
||||
|
||||
- You can create a file named `assets/override/config.json` and add the url to the Mattermost server that you will use to develop:
|
||||
`{
|
||||
"DefaultServerUrl": "https://pre-release.mattermost.com"
|
||||
}`
|
||||
|
||||
To use a local Mattermost server you will need to configure the "DefaultServerUrl" depending on the emulator you will use:
|
||||
* IOs: "DefaultServerUrl": "http://localhost:8065"
|
||||
* Android: "DefaultServerUrl": "http://10.0.2.2:3000"
|
||||
* Genymotion: "DefaultServerUrl": "http://10.0.3.2:8065"
|
||||
|
||||
- Run application
|
||||
- Start emulator
|
||||
- Start react packager: `$ react-native start`
|
||||
- Run in emulator: `$ react-native run-android`
|
||||
|
||||
# Frequently Asked Questions
|
||||
|
||||
@@ -71,3 +149,11 @@ If your app is working properly, you should see a grey “Connecting…” bar t
|
||||
If you are seeing this message all the time, and your internet connection seems fine:
|
||||
|
||||
Ask your server administrator if the server uses NGINX or another webserver as a reverse proxy. If so, they should check that it is configured correctly for [supporting the websocket connection for APIv4 endpoints](https://docs.mattermost.com/install/install-ubuntu-1604.html#configuring-nginx-as-a-proxy-for-mattermost-server).
|
||||
|
||||
# Issues building app for own device using make build-*
|
||||
|
||||
That command is an internal pipeline command for mattermost mobile to publish the mobile apps to ````Apple App Store```` and ````Google Play Store````. All ````make build-*```` commands should be avoided for this reason.
|
||||
|
||||
To build the modified react native client use the instructions for [Running on Device](http://facebook.github.io/react-native/docs/running-on-device.html) from the [React Native Guide](https://facebook.github.io/react-native/docs/getting-started.html).
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import re
|
||||
|
||||
# To learn about Buck see [Docs](https://buckbuild.com/).
|
||||
# To run your application with Buck:
|
||||
# - install Buck
|
||||
@@ -9,9 +11,8 @@
|
||||
#
|
||||
|
||||
lib_deps = []
|
||||
|
||||
for jarfile in glob(['libs/*.jar']):
|
||||
name = 'jars__' + jarfile[jarfile.rindex('/') + 1: jarfile.rindex('.jar')]
|
||||
name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile)
|
||||
lib_deps.append(':' + name)
|
||||
prebuilt_jar(
|
||||
name = name,
|
||||
@@ -19,7 +20,7 @@ for jarfile in glob(['libs/*.jar']):
|
||||
)
|
||||
|
||||
for aarfile in glob(['libs/*.aar']):
|
||||
name = 'aars__' + aarfile[aarfile.rindex('/') + 1: aarfile.rindex('.aar')]
|
||||
name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile)
|
||||
lib_deps.append(':' + name)
|
||||
android_prebuilt_aar(
|
||||
name = name,
|
||||
@@ -27,39 +28,39 @@ for aarfile in glob(['libs/*.aar']):
|
||||
)
|
||||
|
||||
android_library(
|
||||
name = "all-libs",
|
||||
exported_deps = lib_deps,
|
||||
name = 'all-libs',
|
||||
exported_deps = lib_deps
|
||||
)
|
||||
|
||||
android_library(
|
||||
name = "app-code",
|
||||
srcs = glob([
|
||||
"src/main/java/**/*.java",
|
||||
]),
|
||||
deps = [
|
||||
":all-libs",
|
||||
":build_config",
|
||||
":res",
|
||||
],
|
||||
name = 'app-code',
|
||||
srcs = glob([
|
||||
'src/main/java/**/*.java',
|
||||
]),
|
||||
deps = [
|
||||
':all-libs',
|
||||
':build_config',
|
||||
':res',
|
||||
],
|
||||
)
|
||||
|
||||
android_build_config(
|
||||
name = "build_config",
|
||||
package = "com.mattermost-mobile",
|
||||
name = 'build_config',
|
||||
package = 'com.mattermost.rnbeta',
|
||||
)
|
||||
|
||||
android_resource(
|
||||
name = "res",
|
||||
res = "src/main/res",
|
||||
package = "com.mattermost.rnbeta",
|
||||
name = 'res',
|
||||
res = 'src/main/res',
|
||||
package = 'com.mattermost.rnbeta',
|
||||
)
|
||||
|
||||
android_binary(
|
||||
name = "app",
|
||||
keystore = "//android/keystores:debug",
|
||||
manifest = "src/main/AndroidManifest.xml",
|
||||
package_type = "debug",
|
||||
deps = [
|
||||
":app-code",
|
||||
],
|
||||
name = 'app',
|
||||
package_type = 'debug',
|
||||
manifest = 'src/main/AndroidManifest.xml',
|
||||
keystore = '//android/keystores:debug',
|
||||
deps = [
|
||||
':app-code',
|
||||
],
|
||||
)
|
||||
|
||||
@@ -33,13 +33,6 @@ import com.android.build.OutputFile
|
||||
* // bundleInPaidRelease: true,
|
||||
* // bundleInBeta: true,
|
||||
*
|
||||
* // whether to disable dev mode in custom build variants (by default only disabled in release)
|
||||
* // for example: to disable dev mode in the staging build type (if configured)
|
||||
* devDisabledInStaging: true,
|
||||
* // The configuration property can be in the following formats
|
||||
* // 'devDisabledIn${productFlavor}${buildType}'
|
||||
* // 'devDisabledIn${buildType}'
|
||||
*
|
||||
* // the root of your project, i.e. where "package.json" lives
|
||||
* root: "../../",
|
||||
*
|
||||
@@ -65,21 +58,17 @@ import com.android.build.OutputFile
|
||||
* inputExcludes: ["android/**", "ios/**"],
|
||||
*
|
||||
* // override which node gets called and with what additional arguments
|
||||
* nodeExecutableAndArgs: ["node"],
|
||||
* nodeExecutableAndArgs: ["node"]
|
||||
*
|
||||
* // supply additional arguments to the packager
|
||||
* extraPackagerArgs: []
|
||||
* ]
|
||||
*/
|
||||
|
||||
project.ext.react = [
|
||||
entryFile: "index.js"
|
||||
]
|
||||
|
||||
apply from: "../../node_modules/react-native/react.gradle"
|
||||
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
||||
|
||||
if (System.getenv("SENTRY_ENABLED") == "true") {
|
||||
if (System.getenv("MM_SENTRY_ENABLED") == "true") {
|
||||
apply from: "../../node_modules/react-native-sentry/sentry.gradle"
|
||||
}
|
||||
|
||||
@@ -106,8 +95,8 @@ android {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 23
|
||||
versionCode 85
|
||||
versionName "1.6.0"
|
||||
versionCode 63
|
||||
versionName "1.4.0"
|
||||
multiDexEnabled true
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
@@ -163,8 +152,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':react-native-doc-viewer')
|
||||
compile project(':react-native-video')
|
||||
compile fileTree(dir: "libs", include: ["*.jar"])
|
||||
compile "com.android.support:appcompat-v7:25.0.1"
|
||||
compile 'com.android.support:percent:25.3.1'
|
||||
|
||||
4
android/app/proguard-rules.pro
vendored
4
android/app/proguard-rules.pro
vendored
@@ -50,10 +50,6 @@
|
||||
|
||||
-dontwarn com.facebook.react.**
|
||||
|
||||
# TextLayoutBuilder uses a non-public Android constructor within StaticLayout.
|
||||
# See libs/proxy/src/main/java/com/facebook/fbui/textlayoutbuilder/proxy for details.
|
||||
-dontwarn android.text.StaticLayout
|
||||
|
||||
# okhttp
|
||||
|
||||
-keepattributes Signature
|
||||
|
||||
@@ -56,21 +56,6 @@
|
||||
<activity
|
||||
android:name="com.reactnativenavigation.controllers.NavigationActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"/>
|
||||
<activity
|
||||
android:noHistory="false"
|
||||
android:name="com.mattermost.share.ShareActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/AppTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
// for sharing
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.mattermost.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.InputType;
|
||||
|
||||
import com.facebook.react.views.textinput.ReactEditText;
|
||||
|
||||
public class CustomTextInput extends ReactEditText {
|
||||
private boolean autoScroll = false;
|
||||
|
||||
public CustomTextInput(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
private boolean isMultiline() {
|
||||
return (getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLayoutRequested() {
|
||||
if (isMultiline() && !autoScroll) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setAutoScroll(boolean autoScroll) {
|
||||
this.autoScroll = autoScroll;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.mattermost.components;
|
||||
|
||||
import android.text.InputType;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import com.facebook.react.views.textinput.ReactTextInputManager;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
import com.facebook.react.uimanager.ViewDefaults;
|
||||
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||
|
||||
public class CustomTextInputManager extends ReactTextInputManager {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "CustomTextInput";
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomTextInput createViewInstance(ThemedReactContext context) {
|
||||
CustomTextInput editText = new CustomTextInput(context);
|
||||
int inputType = editText.getInputType();
|
||||
editText.setInputType(inputType & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
|
||||
editText.setReturnKeyType("done");
|
||||
editText.setTextSize(
|
||||
TypedValue.COMPLEX_UNIT_PX,
|
||||
(int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP)));
|
||||
return editText;
|
||||
}
|
||||
|
||||
@ReactProp(name = "autoScroll", defaultBoolean = false)
|
||||
public void setAutoScroll(CustomTextInput view, boolean autoScroll) {
|
||||
view.setAutoScroll(autoScroll);
|
||||
}
|
||||
}
|
||||
@@ -56,21 +56,8 @@ public class CustomPushNotification extends PushNotification {
|
||||
if (notificationId != -1) {
|
||||
channelIdToNotificationCount.remove(channelId);
|
||||
channelIdToNotification.remove(channelId);
|
||||
if (context != null) {
|
||||
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancel(notificationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearNotification(Context mContext, int notificationId, String channelId) {
|
||||
if (notificationId != -1) {
|
||||
channelIdToNotificationCount.remove(channelId);
|
||||
channelIdToNotification.remove(channelId);
|
||||
if (mContext != null) {
|
||||
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancel(notificationId);
|
||||
}
|
||||
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancel(notificationId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,15 +197,9 @@ public class CustomPushNotification extends PushNotification {
|
||||
String summaryTitle = String.format("%s (%d)", title, numMessages);
|
||||
|
||||
Notification.InboxStyle style = new Notification.InboxStyle();
|
||||
List<Bundle> bundleArray = channelIdToNotification.get(channelId);
|
||||
List<Bundle> list;
|
||||
if (bundleArray != null) {
|
||||
list = new ArrayList<Bundle>(bundleArray);
|
||||
} else {
|
||||
list = new ArrayList<Bundle>();
|
||||
}
|
||||
List<Bundle> list = new ArrayList<Bundle>(channelIdToNotification.get(channelId));
|
||||
|
||||
for (Bundle data : list) {
|
||||
for (Bundle data : list){
|
||||
String msg = data.getString("message");
|
||||
if (msg != message) {
|
||||
style.addLine(data.getString("message"));
|
||||
@@ -284,8 +265,8 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
boolean vibrate = notificationPreferences.getShouldVibrate();
|
||||
if (vibrate) {
|
||||
// use the system default for vibration
|
||||
notification.setDefaults(Notification.DEFAULT_VIBRATE);
|
||||
// Each element then alternates between delay, vibrate, sleep, vibrate, sleep
|
||||
notification.setVibrate(new long[] {1000, 1000, 500, 1000, 500});
|
||||
}
|
||||
|
||||
boolean blink = notificationPreferences.getShouldBlink();
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.mattermost.share.SharePackage;
|
||||
import android.app.Application;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.reactlibrary.RNReactNativeDocViewerPackage;
|
||||
import com.brentvatne.react.ReactVideoPackage;
|
||||
import com.horcrux.svg.SvgPackage;
|
||||
import com.inprogress.reactnativeyoutube.ReactNativeYouTube;
|
||||
import io.sentry.RNSentryPackage;
|
||||
@@ -69,18 +66,10 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
new MattermostPackage(this),
|
||||
new RNSentryPackage(this),
|
||||
new ReactNativeExceptionHandlerPackage(),
|
||||
new ReactNativeYouTube(),
|
||||
new ReactVideoPackage(),
|
||||
new RNReactNativeDocViewerPackage(),
|
||||
new SharePackage()
|
||||
new ReactNativeYouTube()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
@@ -10,8 +10,6 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
|
||||
import com.mattermost.components.CustomTextInputManager;
|
||||
|
||||
public class MattermostPackage implements ReactPackage {
|
||||
private final MainApplication mApplication;
|
||||
|
||||
@@ -29,8 +27,6 @@ public class MattermostPackage implements ReactPackage {
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Arrays.<ViewManager>asList(
|
||||
new CustomTextInputManager()
|
||||
);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,23 +4,20 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.app.IntentService;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
|
||||
public class NotificationDismissService extends IntentService {
|
||||
private Context mContext;
|
||||
|
||||
public NotificationDismissService() {
|
||||
super("notificationDismissService");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
mContext = getApplicationContext();
|
||||
Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
|
||||
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
|
||||
String channelId = bundle.getString("channel_id");
|
||||
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
|
||||
Log.i("ReactNative", "Dismiss notification");
|
||||
CustomPushNotification.clearNotification(notificationId, channelId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,9 +69,7 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
|
||||
Uri defaultUri = RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_NOTIFICATION);
|
||||
if (defaultUri != null) {
|
||||
result.putString("defaultUri", Uri.decode(defaultUri.toString()));
|
||||
}
|
||||
result.putString("defaultUri", Uri.decode(defaultUri.toString()));
|
||||
result.putString("selectedUri", mNotificationPreference.getNotificationSound());
|
||||
result.putBoolean("shouldVibrate", mNotificationPreference.getShouldVibrate());
|
||||
result.putBoolean("shouldBlink", mNotificationPreference.getShouldBlink());
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.app.NotificationManager;
|
||||
import android.app.RemoteInput;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.HeadlessJsTaskService;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
@@ -16,11 +15,9 @@ import com.facebook.react.jstasks.HeadlessJsTaskConfig;
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
|
||||
public class NotificationReplyService extends HeadlessJsTaskService {
|
||||
private Context mContext;
|
||||
|
||||
@Override
|
||||
protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
|
||||
mContext = getApplicationContext();
|
||||
if (CustomPushNotification.KEY_TEXT_REPLY.equals(intent.getAction())) {
|
||||
CharSequence message = getReplyMessage(intent);
|
||||
|
||||
@@ -30,9 +27,8 @@ public class NotificationReplyService extends HeadlessJsTaskService {
|
||||
bundle.putInt("msg_count", CustomPushNotification.getMessageCountInChannel(channelId));
|
||||
|
||||
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
|
||||
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
|
||||
CustomPushNotification.clearNotification(notificationId, channelId);
|
||||
|
||||
Log.i("ReactNative", "Replying service");
|
||||
return new HeadlessJsTaskConfig(
|
||||
"notificationReplied",
|
||||
Arguments.fromBundle(bundle),
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
package com.mattermost.share;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.content.ContentUris;
|
||||
import android.os.Environment;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import java.io.*;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
// Class based on the steveevers DocumentHelper https://gist.github.com/steveevers/a5af24c226f44bb8fdc3
|
||||
|
||||
public class RealPathUtil {
|
||||
public static String getRealPathFromURI(final Context context, final Uri uri) {
|
||||
|
||||
final boolean isKitKatOrNewer = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
|
||||
// DocumentProvider
|
||||
if (isKitKatOrNewer && DocumentsContract.isDocumentUri(context, uri)) {
|
||||
// ExternalStorageProvider
|
||||
if (isExternalStorageDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
if ("primary".equalsIgnoreCase(type)) {
|
||||
return Environment.getExternalStorageDirectory() + "/" + split[1];
|
||||
}
|
||||
} else if (isDownloadsDocument(uri)) {
|
||||
// DownloadsProvider
|
||||
|
||||
final String id = DocumentsContract.getDocumentId(uri);
|
||||
final Uri contentUri = ContentUris.withAppendedId(
|
||||
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
|
||||
|
||||
return getDataColumn(context, contentUri, null, null);
|
||||
} else if (isMediaDocument(uri)) {
|
||||
// MediaProvider
|
||||
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
Uri contentUri = null;
|
||||
if ("image".equals(type)) {
|
||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("video".equals(type)) {
|
||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("audio".equals(type)) {
|
||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
}
|
||||
|
||||
final String selection = "_id=?";
|
||||
final String[] selectionArgs = new String[] {
|
||||
split[1]
|
||||
};
|
||||
|
||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
}
|
||||
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
|
||||
// MediaStore (and general)
|
||||
|
||||
if (isGooglePhotosUri(uri)) {
|
||||
return uri.getLastPathSegment();
|
||||
}
|
||||
|
||||
String path = getDataColumn(context, uri, null, null);
|
||||
|
||||
if (path != null) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Try save to tmp file, and return tmp file path
|
||||
return getPathFromSavingTempFile(context, uri);
|
||||
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getPathFromSavingTempFile(Context context, final Uri uri) {
|
||||
File tmpFile;
|
||||
try {
|
||||
String fileName = uri.getLastPathSegment();
|
||||
tmpFile = File.createTempFile("tmp", fileName, context.getCacheDir());
|
||||
|
||||
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
|
||||
|
||||
FileChannel src = new FileInputStream(pfd.getFileDescriptor()).getChannel();
|
||||
FileChannel dst = new FileOutputStream(tmpFile).getChannel();
|
||||
dst.transferFrom(src, 0, src.size());
|
||||
src.close();
|
||||
dst.close();
|
||||
} catch (IOException ex) {
|
||||
return null;
|
||||
}
|
||||
return tmpFile.getAbsolutePath();
|
||||
}
|
||||
|
||||
public static String getDataColumn(Context context, Uri uri, String selection,
|
||||
String[] selectionArgs) {
|
||||
|
||||
Cursor cursor = null;
|
||||
final String column = "_data";
|
||||
final String[] projection = {
|
||||
column
|
||||
};
|
||||
|
||||
try {
|
||||
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
|
||||
null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
final int index = cursor.getColumnIndexOrThrow(column);
|
||||
return cursor.getString(index);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static boolean isExternalStorageDocument(Uri uri) {
|
||||
return "com.android.externalstorage.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
public static boolean isDownloadsDocument(Uri uri) {
|
||||
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
public static boolean isMediaDocument(Uri uri) {
|
||||
return "com.android.providers.media.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
public static boolean isGooglePhotosUri(Uri uri) {
|
||||
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
public static String getExtension(String uri) {
|
||||
if (uri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int dot = uri.lastIndexOf(".");
|
||||
if (dot >= 0) {
|
||||
return uri.substring(dot);
|
||||
} else {
|
||||
// No extension.
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static String getMimeType(File file) {
|
||||
|
||||
String extension = getExtension(file.getName());
|
||||
|
||||
if (extension.length() > 0)
|
||||
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.substring(1));
|
||||
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
public static String getMimeType(String filePath) {
|
||||
File file = new File(filePath);
|
||||
return getMimeType(file);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.mattermost.share;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.facebook.react.ReactActivity;
|
||||
|
||||
public class ShareActivity extends ReactActivity {
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "MattermostShare";
|
||||
}
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
package com.mattermost.share;
|
||||
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.Callback;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import java.io.InputStream;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.IOException;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class ShareModule extends ReactContextBaseJavaModule {
|
||||
private final OkHttpClient client = new OkHttpClient();
|
||||
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
public ShareModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "MattermostShare";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void clear() {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
|
||||
if (currentActivity != null) {
|
||||
Intent intent = currentActivity.getIntent();
|
||||
intent.setAction("");
|
||||
intent.removeExtra(Intent.EXTRA_TEXT);
|
||||
intent.removeExtra(Intent.EXTRA_STREAM);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void close(ReadableMap data) {
|
||||
getCurrentActivity().finish();
|
||||
|
||||
if (data != null) {
|
||||
ReadableArray files = data.getArray("files");
|
||||
String serverUrl = data.getString("url");
|
||||
String token = data.getString("token");
|
||||
JSONObject postData = buildPostObject(data);
|
||||
|
||||
if (files.size() > 0) {
|
||||
uploadFiles(serverUrl, token, files, postData);
|
||||
} else {
|
||||
try {
|
||||
post(serverUrl, token, postData);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void data(Promise promise) {
|
||||
promise.resolve(processIntent());
|
||||
}
|
||||
|
||||
public WritableArray processIntent() {
|
||||
WritableMap map = Arguments.createMap();
|
||||
WritableArray items = Arguments.createArray();
|
||||
|
||||
String text = "";
|
||||
String type = "";
|
||||
String action = "";
|
||||
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
|
||||
if (currentActivity != null) {
|
||||
Intent intent = currentActivity.getIntent();
|
||||
action = intent.getAction();
|
||||
type = intent.getType();
|
||||
if (type == null) {
|
||||
type = "";
|
||||
}
|
||||
|
||||
if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type)) {
|
||||
text = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
map.putString("value", text);
|
||||
map.putString("type", type);
|
||||
items.pushMap(map);
|
||||
} else if (Intent.ACTION_SEND.equals(action)) {
|
||||
Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
text = "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri);
|
||||
map.putString("value", text);
|
||||
map.putString("type", type);
|
||||
items.pushMap(map);
|
||||
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
|
||||
ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||
for (Uri uri : uris) {
|
||||
String filePath = RealPathUtil.getRealPathFromURI(currentActivity, uri);
|
||||
map = Arguments.createMap();
|
||||
text = "file://" + filePath;
|
||||
map.putString("value", text);
|
||||
map.putString("type", RealPathUtil.getMimeType(filePath));
|
||||
items.pushMap(map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private JSONObject buildPostObject(ReadableMap data) {
|
||||
JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put("user_id", data.getString("currentUserId"));
|
||||
json.put("channel_id", data.getString("channelId"));
|
||||
json.put("message", data.getString("value"));
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
private void post(String serverUrl, String token, JSONObject postData) throws IOException {
|
||||
RequestBody body = RequestBody.create(JSON, postData.toString());
|
||||
Request request = new Request.Builder()
|
||||
.header("Authorization", "BEARER " + token)
|
||||
.url(serverUrl + "/api/v4/posts")
|
||||
.post(body)
|
||||
.build();
|
||||
Response response = client.newCall(request).execute();
|
||||
}
|
||||
|
||||
private void uploadFiles(String serverUrl, String token, ReadableArray files, JSONObject postData) {
|
||||
try {
|
||||
MultipartBody.Builder builder = new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM);
|
||||
|
||||
for(int i = 0 ; i < files.size() ; i++) {
|
||||
ReadableMap file = files.getMap(i);
|
||||
String filePath = file.getString("fullPath").replaceFirst("file://", "");
|
||||
File fileInfo = new File(filePath);
|
||||
if (fileInfo.exists()) {
|
||||
final MediaType MEDIA_TYPE = MediaType.parse(file.getString("mimeType"));
|
||||
builder.addFormDataPart("files", file.getString("filename"), RequestBody.create(MEDIA_TYPE, fileInfo));
|
||||
}
|
||||
}
|
||||
|
||||
builder.addFormDataPart("channel_id", postData.getString("channel_id"));
|
||||
RequestBody body = builder.build();
|
||||
Request request = new Request.Builder()
|
||||
.header("Authorization", "BEARER " + token)
|
||||
.url(serverUrl + "/api/v4/files")
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (response.isSuccessful()) {
|
||||
String responseData = response.body().string();
|
||||
JSONObject responseJson = new JSONObject(responseData);
|
||||
JSONArray fileInfoArray = responseJson.getJSONArray("file_infos");
|
||||
JSONArray file_ids = new JSONArray();
|
||||
for(int i = 0 ; i < fileInfoArray.length() ; i++) {
|
||||
JSONObject fileInfo = fileInfoArray.getJSONObject(i);
|
||||
file_ids.put(fileInfo.getString("id"));
|
||||
}
|
||||
postData.put("file_ids", file_ids);
|
||||
post(serverUrl, token, postData);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.mattermost.share;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.facebook.react.ReactPackage;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class SharePackage implements ReactPackage {
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Arrays.<NativeModule>asList(new ShareModule(reactContext));
|
||||
}
|
||||
|
||||
public List<Class<? extends JavaScriptModule>> createJSModules() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
keystore(
|
||||
name = "debug",
|
||||
properties = "debug.keystore.properties",
|
||||
store = "debug.keystore",
|
||||
visibility = [
|
||||
"PUBLIC",
|
||||
],
|
||||
name = 'debug',
|
||||
store = 'debug.keystore',
|
||||
properties = 'debug.keystore.properties',
|
||||
visibility = [
|
||||
'PUBLIC',
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
rootProject.name = 'Mattermost'
|
||||
include ':react-native-doc-viewer'
|
||||
project(':react-native-doc-viewer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-doc-viewer/android')
|
||||
include ':react-native-video'
|
||||
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
|
||||
include ':react-native-youtube'
|
||||
project(':react-native-youtube').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-youtube/android')
|
||||
include ':react-native-sentry'
|
||||
|
||||
@@ -8,9 +8,9 @@ import {ViewTypes} from 'app/constants';
|
||||
import {UserTypes} from 'mattermost-redux/action_types';
|
||||
import {
|
||||
fetchMyChannelsAndMembers,
|
||||
markChannelAsRead,
|
||||
selectChannel,
|
||||
leaveChannel as serviceLeaveChannel
|
||||
leaveChannel as serviceLeaveChannel,
|
||||
unfavoriteChannel
|
||||
} from 'mattermost-redux/actions/channels';
|
||||
import {getPosts, getPostsBefore, getPostsSince, getPostThread} from 'mattermost-redux/actions/posts';
|
||||
import {getFilesForPost} from 'mattermost-redux/actions/files';
|
||||
@@ -19,7 +19,6 @@ import {getTeamMembersByIds} from 'mattermost-redux/actions/teams';
|
||||
import {getProfilesInChannel} from 'mattermost-redux/actions/users';
|
||||
import {General, Preferences} from 'mattermost-redux/constants';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {
|
||||
getChannelByName,
|
||||
getDirectChannelName,
|
||||
@@ -237,27 +236,20 @@ export function selectInitialChannel(teamId) {
|
||||
if (lastChannelId && myMembers[lastChannelId] &&
|
||||
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)) {
|
||||
handleSelectChannel(lastChannelId)(dispatch, getState);
|
||||
markChannelAsRead(lastChannelId)(dispatch, getState);
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = Object.values(channels).find((c) => c.team_id === teamId && c.name === General.DEFAULT_CHANNEL);
|
||||
let channelId;
|
||||
if (channel) {
|
||||
channelId = channel.id;
|
||||
dispatch(setChannelDisplayName(''));
|
||||
handleSelectChannel(channel.id)(dispatch, getState);
|
||||
} else {
|
||||
// Handle case when the default channel cannot be found
|
||||
// so we need to get the first available channel of the team
|
||||
const channelsInTeam = Object.values(channels).filter((c) => c.team_id === teamId);
|
||||
const firstChannel = channelsInTeam.length ? channelsInTeam[0].id : {id: ''};
|
||||
|
||||
channelId = firstChannel.id;
|
||||
}
|
||||
|
||||
if (channelId) {
|
||||
dispatch(setChannelDisplayName(''));
|
||||
handleSelectChannel(channelId)(dispatch, getState);
|
||||
markChannelAsRead(channelId)(dispatch, getState);
|
||||
handleSelectChannel(firstChannel.id)(dispatch, getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -379,10 +371,13 @@ export function toggleGMChannel(channelId, visible) {
|
||||
export function closeDMChannel(channel) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
|
||||
if (channel.isFavorite) {
|
||||
unfavoriteChannel(channel.id)(dispatch, getState);
|
||||
}
|
||||
|
||||
toggleDMChannel(channel.teammate_id, 'false')(dispatch, getState);
|
||||
if (channel.id === currentChannelId) {
|
||||
if (channel.isCurrent) {
|
||||
selectInitialChannel(state.entities.teams.currentTeamId)(dispatch, getState);
|
||||
}
|
||||
};
|
||||
@@ -391,10 +386,13 @@ export function closeDMChannel(channel) {
|
||||
export function closeGMChannel(channel) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
|
||||
if (channel.isFavorite) {
|
||||
unfavoriteChannel(channel.id)(dispatch, getState);
|
||||
}
|
||||
|
||||
toggleGMChannel(channel.id, 'false')(dispatch, getState);
|
||||
if (channel.id === currentChannelId) {
|
||||
if (channel.isCurrent) {
|
||||
selectInitialChannel(state.entities.teams.currentTeamId)(dispatch, getState);
|
||||
}
|
||||
};
|
||||
@@ -455,31 +453,16 @@ export function increasePostVisibility(channelId, focusedPostId) {
|
||||
const currentPostVisibility = postVisibility[channelId] || 0;
|
||||
|
||||
if (loadingPosts[channelId]) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if we already have the posts that we want to show
|
||||
if (!focusedPostId) {
|
||||
const loadedPostCount = state.entities.posts.postsInChannel[channelId].length;
|
||||
const desiredPostVisibility = currentPostVisibility + ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
|
||||
if (loadedPostCount >= desiredPostVisibility) {
|
||||
// We already have the posts, so we just need to show them
|
||||
dispatch({
|
||||
type: ViewTypes.INCREASE_POST_VISIBILITY,
|
||||
data: channelId,
|
||||
amount: ViewTypes.POST_VISIBILITY_CHUNK_SIZE
|
||||
});
|
||||
|
||||
return;
|
||||
dispatch(batchActions([
|
||||
{
|
||||
type: ViewTypes.LOADING_POSTS,
|
||||
data: true,
|
||||
channelId
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ViewTypes.LOADING_POSTS,
|
||||
data: true,
|
||||
channelId
|
||||
});
|
||||
]));
|
||||
|
||||
const page = Math.floor(currentPostVisibility / ViewTypes.POST_VISIBILITY_CHUNK_SIZE);
|
||||
|
||||
@@ -506,5 +489,7 @@ export function increasePostVisibility(channelId, focusedPostId) {
|
||||
data: false,
|
||||
channelId
|
||||
});
|
||||
|
||||
return posts && posts.order.length >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ export function handleAddChannelMembers(channelId, members) {
|
||||
try {
|
||||
const requests = members.map((m) => dispatch(addChannelMember(channelId, m, getState)));
|
||||
|
||||
return await Promise.all(requests);
|
||||
await Promise.all(requests);
|
||||
} catch (error) {
|
||||
return error;
|
||||
// should be handled by global error handling
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ export function handleRemoveChannelMembers(channelId, members) {
|
||||
try {
|
||||
const requests = members.map((m) => dispatch(removeChannelMember(channelId, m, getState)));
|
||||
|
||||
return await Promise.all(requests);
|
||||
await Promise.all(requests);
|
||||
} catch (error) {
|
||||
return error;
|
||||
// should be handled by global error handling
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {executeCommand as executeCommandService} from 'mattermost-redux/actions/integrations';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
export function executeCommand(message, channelId, rootId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
const teamId = getCurrentTeamId(state);
|
||||
|
||||
const args = {
|
||||
channel_id: channelId,
|
||||
team_id: teamId,
|
||||
root_id: rootId,
|
||||
parent_id: rootId
|
||||
};
|
||||
|
||||
let msg = message;
|
||||
|
||||
let cmdLength = msg.indexOf(' ');
|
||||
if (cmdLength < 0) {
|
||||
cmdLength = msg.length;
|
||||
}
|
||||
|
||||
const cmd = msg.substring(0, cmdLength).toLowerCase();
|
||||
msg = cmd + msg.substring(cmdLength, msg.length);
|
||||
|
||||
return await executeCommandService(msg, args)(dispatch, getState);
|
||||
};
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {uploadProfileImage, updateMe} from 'mattermost-redux/actions/users';
|
||||
import {buildFileUploadData} from 'app/utils/file';
|
||||
|
||||
export function updateUser(user, success, error) {
|
||||
return async (dispatch, getState) => {
|
||||
const result = await updateMe(user)(dispatch, getState);
|
||||
const {data, error: err} = result;
|
||||
if (data && success) {
|
||||
success(data);
|
||||
} else if (err && error) {
|
||||
error({id: err.server_error_id, ...err});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
export function handleUploadProfileImage(image, userId) {
|
||||
return async (dispatch, getState) => {
|
||||
const imageData = buildFileUploadData(image);
|
||||
return await uploadProfileImage(userId, imageData)(dispatch, getState);
|
||||
};
|
||||
}
|
||||
@@ -1,34 +1,17 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {addReaction as serviceAddReaction} from 'mattermost-redux/actions/posts';
|
||||
import {addReaction} from 'mattermost-redux/actions/posts';
|
||||
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
const getPostIdsForThread = makeGetPostIdsForThread();
|
||||
|
||||
export function addReaction(postId, emoji) {
|
||||
return (dispatch) => {
|
||||
dispatch(serviceAddReaction(postId, emoji));
|
||||
dispatch(addRecentEmoji(emoji));
|
||||
};
|
||||
}
|
||||
|
||||
export function addReactionToLatestPost(emoji, rootId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const postIds = rootId ? getPostIdsForThread(state, rootId) : getPostIdsInCurrentChannel(state);
|
||||
const lastPostId = postIds[0];
|
||||
|
||||
dispatch(serviceAddReaction(lastPostId, emoji));
|
||||
dispatch(addRecentEmoji(emoji));
|
||||
};
|
||||
}
|
||||
|
||||
export function addRecentEmoji(emoji) {
|
||||
return {
|
||||
type: ViewTypes.ADD_RECENT_EMOJI,
|
||||
emoji
|
||||
dispatch(addReaction(lastPostId, emoji));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import FormData from 'form-data';
|
||||
import {Platform} from 'react-native';
|
||||
import {uploadFile} from 'mattermost-redux/actions/files';
|
||||
import {parseClientIdsFromFormData} from 'mattermost-redux/utils/file_utils';
|
||||
import {lookupMimeType, parseClientIdsFromFormData} from 'mattermost-redux/utils/file_utils';
|
||||
|
||||
import {buildFileUploadData, generateId} from 'app/utils/file';
|
||||
import {generateId} from 'app/utils/file';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
export function handleUploadFiles(files, rootId) {
|
||||
@@ -18,17 +18,25 @@ export function handleUploadFiles(files, rootId) {
|
||||
const clientIds = [];
|
||||
|
||||
files.forEach((file) => {
|
||||
const fileData = buildFileUploadData(file);
|
||||
const mimeType = lookupMimeType(file.fileName);
|
||||
const extension = file.fileName.split('.').pop().replace('.', '');
|
||||
const clientId = generateId();
|
||||
|
||||
clientIds.push({
|
||||
clientId,
|
||||
localPath: fileData.uri,
|
||||
name: fileData.name,
|
||||
type: fileData.mimeType,
|
||||
extension: fileData.extension
|
||||
localPath: file.uri,
|
||||
name: file.fileName,
|
||||
type: mimeType,
|
||||
extension
|
||||
});
|
||||
|
||||
const fileData = {
|
||||
uri: file.uri,
|
||||
name: file.fileName,
|
||||
type: mimeType,
|
||||
extension
|
||||
};
|
||||
|
||||
formData.append('files', fileData);
|
||||
formData.append('channel_id', channelId);
|
||||
formData.append('client_ids', clientId);
|
||||
@@ -91,14 +99,6 @@ export function handleClearFiles(channelId, rootId) {
|
||||
};
|
||||
}
|
||||
|
||||
export function handleClearFailedFiles(channelId, rootId) {
|
||||
return {
|
||||
type: ViewTypes.CLEAR_FAILED_FILES_FOR_POST_DRAFT,
|
||||
channelId,
|
||||
rootId
|
||||
};
|
||||
}
|
||||
|
||||
export function handleRemoveFile(clientId, channelId, rootId) {
|
||||
return {
|
||||
type: ViewTypes.REMOVE_FILE_FROM_POST_DRAFT,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {getDataRetentionPolicy} from 'mattermost-redux/actions/general';
|
||||
import {GeneralTypes} from 'mattermost-redux/action_types';
|
||||
import {Client, Client4} from 'mattermost-redux/client';
|
||||
|
||||
@@ -27,10 +25,8 @@ export function handlePasswordChanged(password) {
|
||||
|
||||
export function handleSuccessfulLogin() {
|
||||
return async (dispatch, getState) => {
|
||||
const {config, license} = getState().entities.general;
|
||||
const token = Client4.getToken();
|
||||
const url = Client4.getUrl();
|
||||
|
||||
dispatch({
|
||||
type: GeneralTypes.RECEIVED_APP_CREDENTIALS,
|
||||
data: {
|
||||
@@ -42,13 +38,6 @@ export function handleSuccessfulLogin() {
|
||||
Client.setToken(token);
|
||||
Client.setUrl(url);
|
||||
|
||||
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
|
||||
license.IsLicensed === 'true' && license.DataRetention === 'true') {
|
||||
getDataRetentionPolicy()(dispatch, getState);
|
||||
} else {
|
||||
dispatch({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {GeneralTypes, PostTypes} from 'mattermost-redux/action_types';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {markChannelAsRead} from 'mattermost-redux/actions/channels';
|
||||
import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from 'mattermost-redux/actions/general';
|
||||
import {getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general';
|
||||
import {getPosts} from 'mattermost-redux/actions/posts';
|
||||
import {getMyTeams, getMyTeamMembers, selectTeam} from 'mattermost-redux/actions/teams';
|
||||
|
||||
import {recordTime} from 'app/utils/segment';
|
||||
|
||||
import {
|
||||
handleSelectChannel,
|
||||
setChannelDisplayName,
|
||||
@@ -19,24 +14,11 @@ import {
|
||||
|
||||
export function loadConfigAndLicense() {
|
||||
return async (dispatch, getState) => {
|
||||
const {currentUserId} = getState().entities.users;
|
||||
const [configData, licenseData] = await Promise.all([
|
||||
const [config, license] = await Promise.all([
|
||||
getClientConfig()(dispatch, getState),
|
||||
getLicenseConfig()(dispatch, getState)
|
||||
]);
|
||||
|
||||
const config = configData.data || {};
|
||||
const license = licenseData.data || {};
|
||||
|
||||
if (currentUserId) {
|
||||
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
|
||||
license.IsLicensed === 'true' && license.DataRetention === 'true') {
|
||||
getDataRetentionPolicy()(dispatch, getState);
|
||||
} else {
|
||||
dispatch({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
|
||||
}
|
||||
}
|
||||
|
||||
return {config, license};
|
||||
};
|
||||
}
|
||||
@@ -68,11 +50,9 @@ export function loadFromPushNotification(notification) {
|
||||
// when the notification is from the same channel as the current channel
|
||||
// we should get the posts
|
||||
if (channelId === currentChannelId) {
|
||||
markChannelAsRead(channelId, null, false)(dispatch, getState);
|
||||
await retryGetPostsAction(getPosts(channelId), dispatch, getState);
|
||||
} else {
|
||||
// when the notification is from a channel other than the current channel
|
||||
markChannelAsRead(channelId, currentChannelId, false)(dispatch, getState);
|
||||
dispatch(setChannelDisplayName(''));
|
||||
handleSelectChannel(channelId)(dispatch, getState);
|
||||
}
|
||||
@@ -83,44 +63,6 @@ export function purgeOfflineStore() {
|
||||
return {type: General.OFFLINE_STORE_PURGE};
|
||||
}
|
||||
|
||||
export function createPost(post) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentUserId = state.entities.users.currentUserId;
|
||||
|
||||
const timestamp = Date.now();
|
||||
const pendingPostId = post.pending_post_id || `${currentUserId}:${timestamp}`;
|
||||
|
||||
const newPost = {
|
||||
...post,
|
||||
pending_post_id: pendingPostId,
|
||||
create_at: timestamp,
|
||||
update_at: timestamp
|
||||
};
|
||||
|
||||
return Client4.createPost({...newPost, create_at: 0}).then((payload) => {
|
||||
dispatch({
|
||||
type: PostTypes.RECEIVED_POSTS,
|
||||
data: {
|
||||
order: [],
|
||||
posts: {
|
||||
[payload.id]: payload
|
||||
}
|
||||
},
|
||||
channelId: payload.channel_id
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function recordLoadTime(screenName, category) {
|
||||
return async (dispatch, getState) => {
|
||||
const {currentUserId} = getState().entities.users;
|
||||
|
||||
recordTime(screenName, category, currentUserId);
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
loadConfigAndLicense,
|
||||
loadFromPushNotification,
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
import {GeneralTypes} from 'mattermost-redux/action_types';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
export function handleServerUrlChanged(serverUrl) {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch(batchActions([
|
||||
{type: GeneralTypes.CLIENT_CONFIG_RESET},
|
||||
{type: GeneralTypes.CLIENT_LICENSE_RESET},
|
||||
{type: ViewTypes.SERVER_URL_CHANGED, serverUrl}
|
||||
]), getState);
|
||||
dispatch({
|
||||
type: ViewTypes.SERVER_URL_CHANGED,
|
||||
serverUrl
|
||||
}, getState);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
|
||||
import {markChannelAsRead, viewChannel} from 'mattermost-redux/actions/channels';
|
||||
import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
@@ -28,14 +28,13 @@ export function handleTeamChange(teamId, selectChannel = true) {
|
||||
if (selectChannel) {
|
||||
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: ''});
|
||||
|
||||
const lastChannels = state.views.team.lastChannelForTeam[teamId] || [];
|
||||
const lastChannelId = lastChannels[0] || '';
|
||||
const lastChannelId = state.views.team.lastChannelForTeam[teamId] || '';
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
markChannelAsViewed(currentChannelId)(dispatch, getState);
|
||||
viewChannel(lastChannelId, currentChannelId)(dispatch, getState);
|
||||
markChannelAsRead(lastChannelId, currentChannelId)(dispatch, getState);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_SELECT_TEAM'), getState);
|
||||
dispatch(batchActions(actions), getState);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {userTyping as wsUserTyping} from 'mattermost-redux/actions/websocket';
|
||||
|
||||
export function userTyping(channelId, rootId) {
|
||||
return async (dispatch, getState) => {
|
||||
const {websocket} = getState().device;
|
||||
if (websocket.connected) {
|
||||
wsUserTyping(channelId, rootId)(dispatch, getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -3,29 +3,24 @@
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Clipboard, Text} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Text} from 'react-native';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
|
||||
export default class AtMention extends React.PureComponent {
|
||||
class AtMention extends React.PureComponent {
|
||||
static propTypes = {
|
||||
intl: intlShape,
|
||||
isSearchResult: PropTypes.bool,
|
||||
mentionName: PropTypes.string.isRequired,
|
||||
mentionStyle: CustomPropTypes.Style,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onLongPress: PropTypes.func.isRequired,
|
||||
onPostPress: PropTypes.func,
|
||||
textStyle: CustomPropTypes.Style,
|
||||
theme: PropTypes.object.isRequired,
|
||||
usersByUsername: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -47,8 +42,7 @@ export default class AtMention extends React.PureComponent {
|
||||
}
|
||||
|
||||
goToUserProfile = () => {
|
||||
const {navigator, theme} = this.props;
|
||||
const {intl} = this.context;
|
||||
const {intl, navigator, theme} = this.props;
|
||||
|
||||
navigator.push({
|
||||
screen: 'UserProfile',
|
||||
@@ -71,7 +65,7 @@ export default class AtMention extends React.PureComponent {
|
||||
let mentionName = props.mentionName;
|
||||
|
||||
while (mentionName.length > 0) {
|
||||
if (props.usersByUsername.hasOwnProperty(mentionName)) {
|
||||
if (props.usersByUsername[mentionName]) {
|
||||
const user = props.usersByUsername[mentionName];
|
||||
return {
|
||||
username: user.username,
|
||||
@@ -92,30 +86,6 @@ export default class AtMention extends React.PureComponent {
|
||||
};
|
||||
}
|
||||
|
||||
handleLongPress = async () => {
|
||||
const {intl} = this.context;
|
||||
|
||||
const config = await mattermostManaged.getLocalConfig();
|
||||
|
||||
let action;
|
||||
if (config.copyAndPasteProtection !== 'false') {
|
||||
action = {
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.mention.copy_mention',
|
||||
defaultMessage: 'Copy Mention'
|
||||
}),
|
||||
onPress: this.handleCopyMention
|
||||
};
|
||||
}
|
||||
|
||||
this.props.onLongPress(action);
|
||||
}
|
||||
|
||||
handleCopyMention = () => {
|
||||
const {username} = this.state;
|
||||
Clipboard.setString(`@${username}`);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {isSearchResult, mentionName, mentionStyle, onPostPress, textStyle} = this.props;
|
||||
const username = this.state.username;
|
||||
@@ -130,7 +100,6 @@ export default class AtMention extends React.PureComponent {
|
||||
<Text
|
||||
style={textStyle}
|
||||
onPress={isSearchResult ? onPostPress : this.goToUserProfile}
|
||||
onLongPress={this.handleLongPress}
|
||||
>
|
||||
<Text style={mentionStyle}>
|
||||
{'@' + username}
|
||||
@@ -140,3 +109,5 @@ export default class AtMention extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(AtMention);
|
||||
|
||||
@@ -9,10 +9,11 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import AtMention from './at_mention';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
usersByUsername: getUsersByUsername(state)
|
||||
usersByUsername: getUsersByUsername(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
|
||||
import AtMentionItem from 'app/components/autocomplete/at_mention_item';
|
||||
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
|
||||
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
|
||||
import SpecialMentionItem from 'app/components/autocomplete/special_mention_item';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
@@ -25,21 +24,19 @@ export default class AtMention extends PureComponent {
|
||||
defaultChannel: PropTypes.object,
|
||||
inChannel: PropTypes.array,
|
||||
isSearch: PropTypes.bool,
|
||||
listHeight: PropTypes.number,
|
||||
matchTerm: PropTypes.string,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
onResultCountChange: PropTypes.func.isRequired,
|
||||
outChannel: PropTypes.array,
|
||||
postDraft: PropTypes.string,
|
||||
requestStatus: PropTypes.string.isRequired,
|
||||
teamMembers: PropTypes.array,
|
||||
theme: PropTypes.object.isRequired,
|
||||
value: PropTypes.string
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
defaultChannel: {},
|
||||
isSearch: false,
|
||||
value: ''
|
||||
postDraft: '',
|
||||
isSearch: false
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -58,9 +55,6 @@ export default class AtMention extends PureComponent {
|
||||
mentionComplete: false,
|
||||
sections: []
|
||||
});
|
||||
|
||||
this.props.onResultCountChange(0);
|
||||
|
||||
return;
|
||||
} else if (matchTerm === null) {
|
||||
// if the terms did not change but is null then we don't need to do anything
|
||||
@@ -119,8 +113,6 @@ export default class AtMention extends PureComponent {
|
||||
this.setState({
|
||||
sections
|
||||
});
|
||||
|
||||
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,8 +144,8 @@ export default class AtMention extends PureComponent {
|
||||
};
|
||||
|
||||
completeMention = (mention) => {
|
||||
const {cursorPosition, isSearch, onChangeText, value} = this.props;
|
||||
const mentionPart = value.substring(0, cursorPosition);
|
||||
const {cursorPosition, isSearch, onChangeText, postDraft} = this.props;
|
||||
const mentionPart = postDraft.substring(0, cursorPosition);
|
||||
|
||||
let completedDraft;
|
||||
if (isSearch) {
|
||||
@@ -162,8 +154,8 @@ export default class AtMention extends PureComponent {
|
||||
completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `);
|
||||
}
|
||||
|
||||
if (value.length > cursorPosition) {
|
||||
completedDraft += value.substring(cursorPosition);
|
||||
if (postDraft.length > cursorPosition) {
|
||||
completedDraft += postDraft.substring(cursorPosition);
|
||||
}
|
||||
|
||||
onChangeText(completedDraft, true);
|
||||
@@ -203,7 +195,7 @@ export default class AtMention extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {isSearch, listHeight, theme} = this.props;
|
||||
const {isSearch, theme} = this.props;
|
||||
const {mentionComplete, sections} = this.state;
|
||||
|
||||
if (sections.length === 0 || mentionComplete) {
|
||||
@@ -218,11 +210,10 @@ export default class AtMention extends PureComponent {
|
||||
<SectionList
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyExtractor={this.keyExtractor}
|
||||
style={[style.listView, isSearch ? [style.search, {height: listHeight}] : null]}
|
||||
style={[style.listView, isSearch ? style.search : null]}
|
||||
sections={sections}
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
ItemSeparatorComponent={AutocompleteDivider}
|
||||
initialNumToRender={10}
|
||||
/>
|
||||
);
|
||||
@@ -235,7 +226,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
search: {
|
||||
minHeight: 125
|
||||
height: 250
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -19,10 +19,25 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import AtMention from './at_mention';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {cursorPosition, isSearch} = ownProps;
|
||||
const {cursorPosition, isSearch, rootId} = ownProps;
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
|
||||
const value = ownProps.value.substring(0, cursorPosition);
|
||||
let postDraft = '';
|
||||
if (isSearch) {
|
||||
postDraft = state.views.search;
|
||||
} else if (ownProps.rootId) {
|
||||
const threadDraft = state.views.thread.drafts[rootId];
|
||||
if (threadDraft) {
|
||||
postDraft = threadDraft.draft;
|
||||
}
|
||||
} else if (currentChannelId) {
|
||||
const channelDraft = state.views.channel.drafts[currentChannelId];
|
||||
if (channelDraft) {
|
||||
postDraft = channelDraft.draft;
|
||||
}
|
||||
}
|
||||
|
||||
const value = postDraft.substring(0, cursorPosition);
|
||||
const matchTerm = getMatchTermForAtMention(value, isSearch);
|
||||
|
||||
let teamMembers;
|
||||
@@ -39,12 +54,14 @@ function mapStateToProps(state, ownProps) {
|
||||
currentChannelId,
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
defaultChannel: getDefaultChannel(state),
|
||||
postDraft,
|
||||
matchTerm,
|
||||
teamMembers,
|
||||
inChannel,
|
||||
outChannel,
|
||||
requestStatus: state.requests.users.autocompleteUsers.status,
|
||||
theme: getTheme(state)
|
||||
theme: getTheme(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from 'react-native';
|
||||
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class AtMentionItem extends PureComponent {
|
||||
static propTypes = {
|
||||
@@ -60,14 +60,19 @@ export default class AtMentionItem extends PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
row: {
|
||||
paddingVertical: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
},
|
||||
rowPicture: {
|
||||
marginHorizontal: 8,
|
||||
|
||||
@@ -16,7 +16,8 @@ function mapStateToProps(state, ownProps) {
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
username: user.username,
|
||||
theme: getTheme(state)
|
||||
theme: getTheme(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Keyboard,
|
||||
Platform,
|
||||
View
|
||||
} from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import AtMention from './at_mention';
|
||||
import ChannelMention from './channel_mention';
|
||||
import EmojiSuggestion from './emoji_suggestion';
|
||||
import SlashSuggestion from './slash_suggestion';
|
||||
|
||||
export default class Autocomplete extends PureComponent {
|
||||
static propTypes = {
|
||||
deviceHeight: PropTypes.number,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
rootId: PropTypes.string,
|
||||
isSearch: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
value: PropTypes.string
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isSearch: false
|
||||
};
|
||||
|
||||
state = {
|
||||
cursorPosition: 0,
|
||||
atMentionCount: 0,
|
||||
channelMentionCount: 0,
|
||||
emojiCount: 0,
|
||||
commandCount: 0,
|
||||
keyboardOffset: 0
|
||||
};
|
||||
|
||||
handleSelectionChange = (event) => {
|
||||
this.setState({
|
||||
cursorPosition: event.nativeEvent.selection.end
|
||||
});
|
||||
};
|
||||
|
||||
handleAtMentionCountChange = (atMentionCount) => {
|
||||
this.setState({atMentionCount});
|
||||
};
|
||||
|
||||
handleChannelMentionCountChange = (channelMentionCount) => {
|
||||
this.setState({channelMentionCount});
|
||||
};
|
||||
|
||||
handleEmojiCountChange = (emojiCount) => {
|
||||
this.setState({emojiCount});
|
||||
};
|
||||
|
||||
handleCommandCountChange = (commandCount) => {
|
||||
this.setState({commandCount});
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow);
|
||||
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.keyboardDidShowListener.remove();
|
||||
this.keyboardDidHideListener.remove();
|
||||
}
|
||||
|
||||
keyboardDidShow = (e) => {
|
||||
const {height} = e.endCoordinates;
|
||||
this.setState({keyboardOffset: height});
|
||||
}
|
||||
|
||||
keyboardDidHide = () => {
|
||||
this.setState({keyboardOffset: 0});
|
||||
}
|
||||
|
||||
listHeight() {
|
||||
let offset = Platform.select({ios: 65, android: 75});
|
||||
if (DeviceInfo.getModel() === 'iPhone X') {
|
||||
offset = 90;
|
||||
}
|
||||
return this.props.deviceHeight - offset - this.state.keyboardOffset;
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
const wrapperStyle = [];
|
||||
const containerStyle = [];
|
||||
if (this.props.isSearch) {
|
||||
wrapperStyle.push(style.base, style.searchContainer);
|
||||
containerStyle.push(style.content);
|
||||
} else {
|
||||
containerStyle.push(style.base, style.container);
|
||||
}
|
||||
|
||||
// We always need to render something, but we only draw the borders when we have results to show
|
||||
const {atMentionCount, channelMentionCount, emojiCount, commandCount} = this.state;
|
||||
if (atMentionCount + channelMentionCount + emojiCount + commandCount > 0) {
|
||||
if (this.props.isSearch) {
|
||||
wrapperStyle.push(style.bordersSearch);
|
||||
} else {
|
||||
containerStyle.push(style.borders);
|
||||
}
|
||||
}
|
||||
const listHeight = this.listHeight();
|
||||
return (
|
||||
<View style={wrapperStyle}>
|
||||
<View style={containerStyle}>
|
||||
<AtMention
|
||||
listHeight={listHeight}
|
||||
cursorPosition={this.state.cursorPosition}
|
||||
onResultCountChange={this.handleAtMentionCountChange}
|
||||
{...this.props}
|
||||
/>
|
||||
<ChannelMention
|
||||
listHeight={listHeight}
|
||||
cursorPosition={this.state.cursorPosition}
|
||||
onResultCountChange={this.handleChannelMentionCountChange}
|
||||
{...this.props}
|
||||
/>
|
||||
<EmojiSuggestion
|
||||
cursorPosition={this.state.cursorPosition}
|
||||
onResultCountChange={this.handleEmojiCountChange}
|
||||
{...this.props}
|
||||
/>
|
||||
<SlashSuggestion
|
||||
onResultCountChange={this.handleCommandCountChange}
|
||||
{...this.props}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
base: {
|
||||
left: 0,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
right: 0
|
||||
},
|
||||
borders: {
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderBottomWidth: 0
|
||||
},
|
||||
bordersSearch: {
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
},
|
||||
container: {
|
||||
bottom: 0,
|
||||
maxHeight: 200
|
||||
},
|
||||
content: {
|
||||
flex: 1
|
||||
},
|
||||
searchContainer: {
|
||||
flex: 1,
|
||||
...Platform.select({
|
||||
android: {
|
||||
top: 46
|
||||
},
|
||||
ios: {
|
||||
top: 44
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class AutocompleteDivider extends PureComponent {
|
||||
static propTypes = {
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View style={style.divider}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
divider: {
|
||||
height: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import AutocompleteDivider from './autocomplete_divider';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(AutocompleteDivider);
|
||||
@@ -40,7 +40,11 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
paddingLeft: 8,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
},
|
||||
sectionText: {
|
||||
fontSize: 12,
|
||||
|
||||
@@ -8,7 +8,6 @@ import {SectionList} from 'react-native';
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
|
||||
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
|
||||
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
|
||||
import ChannelMentionItem from 'app/components/autocomplete/channel_mention_item';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
@@ -21,22 +20,20 @@ export default class ChannelMention extends PureComponent {
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
cursorPosition: PropTypes.number.isRequired,
|
||||
isSearch: PropTypes.bool,
|
||||
listHeight: PropTypes.number,
|
||||
matchTerm: PropTypes.string,
|
||||
myChannels: PropTypes.array,
|
||||
otherChannels: PropTypes.array,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
onResultCountChange: PropTypes.func.isRequired,
|
||||
postDraft: PropTypes.string,
|
||||
privateChannels: PropTypes.array,
|
||||
publicChannels: PropTypes.array,
|
||||
requestStatus: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
value: PropTypes.string
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isSearch: false,
|
||||
value: ''
|
||||
postDraft: '',
|
||||
isSearch: false
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -56,9 +53,6 @@ export default class ChannelMention extends PureComponent {
|
||||
mentionComplete: false,
|
||||
sections: []
|
||||
});
|
||||
|
||||
this.props.onResultCountChange(0);
|
||||
|
||||
return;
|
||||
} else if (matchTerm === null) {
|
||||
// if the terms did not change but is null then we don't need to do anything
|
||||
@@ -118,14 +112,12 @@ export default class ChannelMention extends PureComponent {
|
||||
this.setState({
|
||||
sections
|
||||
});
|
||||
|
||||
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
|
||||
}
|
||||
}
|
||||
|
||||
completeMention = (mention) => {
|
||||
const {cursorPosition, isSearch, onChangeText, value} = this.props;
|
||||
const mentionPart = value.substring(0, cursorPosition);
|
||||
const {cursorPosition, isSearch, onChangeText, postDraft} = this.props;
|
||||
const mentionPart = postDraft.substring(0, cursorPosition);
|
||||
|
||||
let completedDraft;
|
||||
if (isSearch) {
|
||||
@@ -135,8 +127,8 @@ export default class ChannelMention extends PureComponent {
|
||||
completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~${mention} `);
|
||||
}
|
||||
|
||||
if (value.length > cursorPosition) {
|
||||
completedDraft += value.substring(cursorPosition);
|
||||
if (postDraft.length > cursorPosition) {
|
||||
completedDraft += postDraft.substring(cursorPosition);
|
||||
}
|
||||
|
||||
onChangeText(completedDraft, true);
|
||||
@@ -167,7 +159,7 @@ export default class ChannelMention extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {isSearch, listHeight, theme} = this.props;
|
||||
const {isSearch, theme} = this.props;
|
||||
const {mentionComplete, sections} = this.state;
|
||||
|
||||
if (sections.length === 0 || mentionComplete) {
|
||||
@@ -182,11 +174,10 @@ export default class ChannelMention extends PureComponent {
|
||||
<SectionList
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyExtractor={this.keyExtractor}
|
||||
style={[style.listView, isSearch ? [style.search, {height: listHeight}] : null]}
|
||||
style={[style.listView, isSearch ? style.search : null]}
|
||||
sections={sections}
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
ItemSeparatorComponent={AutocompleteDivider}
|
||||
initialNumToRender={10}
|
||||
/>
|
||||
);
|
||||
@@ -199,7 +190,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
search: {
|
||||
minHeight: 125
|
||||
height: 250
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {searchChannels} from 'mattermost-redux/actions/channels';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {
|
||||
@@ -19,9 +20,25 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import ChannelMention from './channel_mention';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {cursorPosition, isSearch} = ownProps;
|
||||
const {cursorPosition, isSearch, rootId} = ownProps;
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
|
||||
const value = ownProps.value.substring(0, cursorPosition);
|
||||
let postDraft = '';
|
||||
if (isSearch) {
|
||||
postDraft = state.views.search;
|
||||
} else if (rootId) {
|
||||
const threadDraft = state.views.thread.drafts[rootId];
|
||||
if (threadDraft) {
|
||||
postDraft = threadDraft.draft;
|
||||
}
|
||||
} else if (currentChannelId) {
|
||||
const channelDraft = state.views.channel.drafts[currentChannelId];
|
||||
if (channelDraft) {
|
||||
postDraft = channelDraft.draft;
|
||||
}
|
||||
}
|
||||
|
||||
const value = postDraft.substring(0, cursorPosition);
|
||||
const matchTerm = getMatchTermForChannelMention(value, isSearch);
|
||||
|
||||
let myChannels;
|
||||
@@ -37,12 +54,14 @@ function mapStateToProps(state, ownProps) {
|
||||
}
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
myChannels,
|
||||
otherChannels,
|
||||
publicChannels,
|
||||
privateChannels,
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
matchTerm,
|
||||
postDraft,
|
||||
requestStatus: state.requests.channels.getChannels.status,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class ChannelMentionItem extends PureComponent {
|
||||
static propTypes = {
|
||||
@@ -46,14 +46,19 @@ export default class ChannelMentionItem extends PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
row: {
|
||||
padding: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
},
|
||||
rowDisplayName: {
|
||||
fontSize: 13,
|
||||
|
||||
@@ -15,7 +15,8 @@ function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
displayName: channel.display_name,
|
||||
name: channel.name,
|
||||
theme: getTheme(state)
|
||||
theme: getTheme(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,10 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
|
||||
import Emoji from 'app/components/emoji';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
const EMOJI_REGEX = /(^|\s|^\+|^-)(:([^:\s]*))$/i;
|
||||
const EMOJI_REGEX_WITHOUT_PREFIX = /\B(:([^:\s]*))$/i;
|
||||
|
||||
export default class EmojiSuggestion extends Component {
|
||||
static propTypes = {
|
||||
@@ -24,32 +22,25 @@ export default class EmojiSuggestion extends Component {
|
||||
}).isRequired,
|
||||
cursorPosition: PropTypes.number,
|
||||
emojis: PropTypes.array.isRequired,
|
||||
isSearch: PropTypes.bool,
|
||||
fuse: PropTypes.object.isRequired,
|
||||
postDraft: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
onResultCountChange: PropTypes.func.isRequired,
|
||||
rootId: PropTypes.string,
|
||||
value: PropTypes.string
|
||||
rootId: PropTypes.string
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
defaultChannel: {},
|
||||
value: ''
|
||||
postDraft: ''
|
||||
};
|
||||
|
||||
state = {
|
||||
active: false,
|
||||
dataSource: []
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.isSearch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const regex = EMOJI_REGEX;
|
||||
const match = nextProps.value.substring(0, nextProps.cursorPosition).match(regex);
|
||||
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(regex);
|
||||
|
||||
if (!match || this.state.emojiComplete) {
|
||||
this.setState({
|
||||
@@ -57,61 +48,43 @@ export default class EmojiSuggestion extends Component {
|
||||
matchTerm: null,
|
||||
emojiComplete: false
|
||||
});
|
||||
|
||||
this.props.onResultCountChange(0);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const matchTerm = match[3];
|
||||
|
||||
const matchTermChanged = matchTerm !== this.state.matchTerm;
|
||||
if (matchTermChanged) {
|
||||
if (matchTerm !== this.state.matchTerm) {
|
||||
this.setState({
|
||||
matchTerm
|
||||
});
|
||||
}
|
||||
|
||||
if (matchTermChanged) {
|
||||
this.handleFuzzySearch(matchTerm, nextProps);
|
||||
} else if (!matchTerm.length) {
|
||||
let data = [];
|
||||
if (matchTerm.length) {
|
||||
data = nextProps.emojis.filter((emoji) => emoji.startsWith(matchTerm.toLowerCase())).sort();
|
||||
} else {
|
||||
const initialEmojis = [...nextProps.emojis];
|
||||
initialEmojis.splice(0, 300);
|
||||
const data = initialEmojis.sort();
|
||||
|
||||
this.setEmojiData(data);
|
||||
data = initialEmojis.sort();
|
||||
}
|
||||
}
|
||||
|
||||
handleFuzzySearch = async (matchTerm, props) => {
|
||||
const {emojis, fuse} = props;
|
||||
|
||||
const results = await fuse.search(matchTerm.toLowerCase());
|
||||
const data = results.map((index) => emojis[index]);
|
||||
this.setEmojiData(data);
|
||||
};
|
||||
|
||||
setEmojiData = (data) => {
|
||||
this.setState({
|
||||
active: data.length > 0,
|
||||
active: data.length,
|
||||
dataSource: data
|
||||
});
|
||||
|
||||
this.props.onResultCountChange(data.length);
|
||||
};
|
||||
}
|
||||
|
||||
completeSuggestion = (emoji) => {
|
||||
const {actions, cursorPosition, onChangeText, value, rootId} = this.props;
|
||||
const emojiPart = value.substring(0, cursorPosition);
|
||||
const {actions, cursorPosition, onChangeText, postDraft, rootId} = this.props;
|
||||
const emojiPart = postDraft.substring(0, cursorPosition);
|
||||
|
||||
if (emojiPart.startsWith('+:')) {
|
||||
actions.addReactionToLatestPost(emoji, rootId);
|
||||
onChangeText('');
|
||||
} else {
|
||||
let completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `:${emoji}: `);
|
||||
let completedDraft = emojiPart.replace(EMOJI_REGEX, `:${emoji}: `);
|
||||
|
||||
if (value.length > cursorPosition) {
|
||||
completedDraft += value.substring(cursorPosition);
|
||||
if (postDraft.length > cursorPosition) {
|
||||
completedDraft += postDraft.substring(cursorPosition);
|
||||
}
|
||||
|
||||
onChangeText(completedDraft);
|
||||
@@ -163,7 +136,6 @@ export default class EmojiSuggestion extends Component {
|
||||
data={this.state.dataSource}
|
||||
keyExtractor={this.keyExtractor}
|
||||
renderItem={this.renderItem}
|
||||
ItemSeparatorComponent={AutocompleteDivider}
|
||||
pageSize={10}
|
||||
initialListSize={10}
|
||||
/>
|
||||
@@ -189,7 +161,13 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 8,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -12,37 +12,39 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {EmojiIndicesByAlias} from 'app/utils/emojis';
|
||||
|
||||
import EmojiSuggestion from './emoji_suggestion';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
const getEmojisByName = createSelector(
|
||||
getCustomEmojisByName,
|
||||
(customEmojis) => {
|
||||
const emoticons = new Set();
|
||||
const emoticons = [];
|
||||
for (const [key] of [...EmojiIndicesByAlias.entries(), ...customEmojis.entries()]) {
|
||||
emoticons.add(key);
|
||||
emoticons.push(key);
|
||||
}
|
||||
|
||||
return Array.from(emoticons);
|
||||
return emoticons;
|
||||
}
|
||||
);
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const options = {
|
||||
shouldSort: true,
|
||||
threshold: 0.3,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
minMatchCharLength: 2,
|
||||
maxPatternLength: 32
|
||||
};
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {currentChannelId} = state.entities.channels;
|
||||
const emojis = getEmojisByName(state);
|
||||
const list = emojis.length ? emojis : [];
|
||||
const fuse = new Fuse(list, options);
|
||||
|
||||
let postDraft;
|
||||
if (ownProps.rootId) {
|
||||
const threadDraft = state.views.thread.drafts[ownProps.rootId];
|
||||
if (threadDraft) {
|
||||
postDraft = threadDraft.draft;
|
||||
}
|
||||
} else if (currentChannelId) {
|
||||
const channelDraft = state.views.channel.drafts[currentChannelId];
|
||||
if (channelDraft) {
|
||||
postDraft = channelDraft.draft;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fuse,
|
||||
emojis,
|
||||
postDraft,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,20 +1,88 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import AtMention from './at_mention';
|
||||
import ChannelMention from './channel_mention';
|
||||
import EmojiSuggestion from './emoji_suggestion';
|
||||
|
||||
import {getDimensions} from 'app/selectors/device';
|
||||
|
||||
import Autocomplete from './autocomplete';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const {deviceHeight} = getDimensions(state);
|
||||
return {
|
||||
deviceHeight,
|
||||
theme: getTheme(state)
|
||||
export default class Autocomplete extends PureComponent {
|
||||
static propTypes = {
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
rootId: PropTypes.string,
|
||||
isSearch: PropTypes.bool
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isSearch: false
|
||||
};
|
||||
|
||||
state = {
|
||||
cursorPosition: 0
|
||||
};
|
||||
|
||||
handleSelectionChange = (event) => {
|
||||
this.setState({
|
||||
cursorPosition: event.nativeEvent.selection.end
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const searchContainer = this.props.isSearch ? style.searchContainer : null;
|
||||
const container = this.props.isSearch ? null : style.container;
|
||||
return (
|
||||
<View style={searchContainer}>
|
||||
<View style={container}>
|
||||
<AtMention
|
||||
cursorPosition={this.state.cursorPosition}
|
||||
{...this.props}
|
||||
/>
|
||||
<ChannelMention
|
||||
cursorPosition={this.state.cursorPosition}
|
||||
{...this.props}
|
||||
/>
|
||||
<EmojiSuggestion
|
||||
cursorPosition={this.state.cursorPosition}
|
||||
{...this.props}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null, null, {withRef: true})(Autocomplete);
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
maxHeight: 200,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
searchContainer: {
|
||||
elevation: 5,
|
||||
flex: 1,
|
||||
left: 0,
|
||||
maxHeight: 250,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
zIndex: 5,
|
||||
...Platform.select({
|
||||
android: {
|
||||
top: 47
|
||||
},
|
||||
ios: {
|
||||
top: 64
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {getAutocompleteCommands} from 'mattermost-redux/actions/integrations';
|
||||
import {getAutocompleteCommandsList} from 'mattermost-redux/selectors/entities/integrations';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import SlashSuggestion from './slash_suggestion';
|
||||
|
||||
// TODO: Remove when all below commands have been implemented
|
||||
const COMMANDS_TO_IMPLEMENT_LATER = ['collapse', 'expand', 'join', 'open', 'leave', 'logout', 'msg', 'grpmsg'];
|
||||
const NON_MOBILE_COMMANDS = ['rename', 'invite_people', 'shortcuts', 'search', 'help', 'settings', 'remove'];
|
||||
|
||||
const COMMANDS_TO_HIDE_ON_MOBILE = [...COMMANDS_TO_IMPLEMENT_LATER, ...NON_MOBILE_COMMANDS];
|
||||
|
||||
const mobileCommandsSelector = createSelector(
|
||||
getAutocompleteCommandsList,
|
||||
(commands) => {
|
||||
return commands.filter((command) => !COMMANDS_TO_HIDE_ON_MOBILE.includes(command.trigger));
|
||||
}
|
||||
);
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
commands: mobileCommandsSelector(state),
|
||||
commandsRequest: state.requests.integrations.getAutocompleteCommands,
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getAutocompleteCommands
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SlashSuggestion);
|
||||
@@ -1,167 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
FlatList
|
||||
} from 'react-native';
|
||||
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import SlashSuggestionItem from './slash_suggestion_item';
|
||||
|
||||
const SLASH_REGEX = /(^\/)([a-zA-Z-]*)$/;
|
||||
const TIME_BEFORE_NEXT_COMMAND_REQUEST = 1000 * 60 * 5;
|
||||
|
||||
export default class SlashSuggestion extends Component {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
getAutocompleteCommands: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
commands: PropTypes.array,
|
||||
commandsRequest: PropTypes.object.isRequired,
|
||||
isSearch: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
onResultCountChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
defaultChannel: {},
|
||||
value: ''
|
||||
};
|
||||
|
||||
state = {
|
||||
active: false,
|
||||
suggestionComplete: false,
|
||||
dataSource: [],
|
||||
lastCommandRequest: 0
|
||||
};
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.isSearch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {currentTeamId} = this.props;
|
||||
const {
|
||||
commands: nextCommands,
|
||||
commandsRequest: nextCommandsRequest,
|
||||
currentTeamId: nextTeamId,
|
||||
value: nextValue
|
||||
} = nextProps;
|
||||
|
||||
if (currentTeamId !== nextTeamId) {
|
||||
this.setState({
|
||||
lastCommandRequest: 0
|
||||
});
|
||||
}
|
||||
|
||||
const match = nextValue.match(SLASH_REGEX);
|
||||
|
||||
if (!match || this.state.suggestionComplete) {
|
||||
this.setState({
|
||||
active: false,
|
||||
matchTerm: null,
|
||||
suggestionComplete: false
|
||||
});
|
||||
this.props.onResultCountChange(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataIsStale = Date.now() - this.state.lastCommandRequest > TIME_BEFORE_NEXT_COMMAND_REQUEST;
|
||||
|
||||
if ((!nextCommands.length || dataIsStale) && nextCommandsRequest.status !== RequestStatus.STARTED) {
|
||||
this.props.actions.getAutocompleteCommands(nextProps.currentTeamId);
|
||||
this.setState({
|
||||
lastCommandRequest: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
const matchTerm = match[2];
|
||||
|
||||
const data = this.filterSlashSuggestions(matchTerm, nextCommands);
|
||||
|
||||
this.setState({
|
||||
active: data.length,
|
||||
dataSource: data
|
||||
});
|
||||
|
||||
this.props.onResultCountChange(data.length);
|
||||
}
|
||||
|
||||
filterSlashSuggestions = (matchTerm, commands) => {
|
||||
return commands.filter((command) => {
|
||||
if (!command.auto_complete) {
|
||||
return false;
|
||||
} else if (!matchTerm) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return command.display_name.startsWith(matchTerm) || command.trigger.startsWith(matchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
completeSuggestion = (command) => {
|
||||
const {onChangeText} = this.props;
|
||||
|
||||
const completedDraft = `/${command} `;
|
||||
|
||||
onChangeText(completedDraft);
|
||||
|
||||
this.setState({
|
||||
active: false,
|
||||
suggestionComplete: true
|
||||
});
|
||||
};
|
||||
|
||||
keyExtractor = (item) => item.id || item.trigger;
|
||||
|
||||
renderItem = ({item}) => (
|
||||
<SlashSuggestionItem
|
||||
displayName={item.display_name}
|
||||
description={item.auto_complete_desc}
|
||||
hint={item.auto_complete_hint}
|
||||
onPress={this.completeSuggestion}
|
||||
theme={this.props.theme}
|
||||
trigger={item.trigger}
|
||||
/>
|
||||
)
|
||||
|
||||
render() {
|
||||
if (!this.state.active) {
|
||||
// If we are not in an active state return null so nothing is rendered
|
||||
// other components are not blocked.
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps='always'
|
||||
style={style.listView}
|
||||
extraData={this.state}
|
||||
data={this.state.dataSource}
|
||||
keyExtractor={this.keyExtractor}
|
||||
renderItem={this.renderItem}
|
||||
pageSize={10}
|
||||
initialListSize={10}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
listView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class SlashSuggestionItem extends PureComponent {
|
||||
static propTypes = {
|
||||
displayName: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
hint: PropTypes.string,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
trigger: PropTypes.string
|
||||
};
|
||||
|
||||
completeSuggestion = () => {
|
||||
const {onPress, trigger} = this.props;
|
||||
onPress(trigger);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
displayName,
|
||||
description,
|
||||
hint,
|
||||
theme,
|
||||
trigger
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={this.completeSuggestion}
|
||||
style={style.row}
|
||||
>
|
||||
<Text style={style.suggestionName}>{`/${displayName || trigger} ${hint}`}</Text>
|
||||
<Text style={style.suggestionDescription}>{description}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
row: {
|
||||
height: 55,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 8,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
},
|
||||
rowDisplayName: {
|
||||
fontSize: 13,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
rowName: {
|
||||
color: theme.centerChannelColor,
|
||||
opacity: 0.6
|
||||
},
|
||||
suggestionDescription: {
|
||||
fontSize: 11,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6)
|
||||
},
|
||||
suggestionName: {
|
||||
fontSize: 13,
|
||||
color: theme.centerChannelColor,
|
||||
marginBottom: 5
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -70,7 +70,13 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
paddingVertical: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
},
|
||||
rowPicture: {
|
||||
marginHorizontal: 8,
|
||||
|
||||
@@ -32,8 +32,8 @@ export default class Badge extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.width = 0;
|
||||
this.mounted = false;
|
||||
this.layoutReady = false;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
@@ -50,12 +50,6 @@ export default class Badge extends PureComponent {
|
||||
this.mounted = true;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.count !== this.props.count) {
|
||||
this.layoutReady = false;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
}
|
||||
@@ -73,27 +67,34 @@ export default class Badge extends PureComponent {
|
||||
};
|
||||
|
||||
onLayout = (e) => {
|
||||
if (!this.layoutReady) {
|
||||
const height = Math.max(e.nativeEvent.layout.height, this.props.minHeight);
|
||||
const borderRadius = height / 2;
|
||||
let width;
|
||||
let width;
|
||||
|
||||
if (e.nativeEvent.layout.width <= e.nativeEvent.layout.height) {
|
||||
width = e.nativeEvent.layout.height;
|
||||
} else {
|
||||
width = e.nativeEvent.layout.width + this.props.extraPaddingHorizontal;
|
||||
if (e.nativeEvent.layout.width <= e.nativeEvent.layout.height) {
|
||||
width = e.nativeEvent.layout.height;
|
||||
} else {
|
||||
width = e.nativeEvent.layout.width + this.props.extraPaddingHorizontal;
|
||||
}
|
||||
width = Math.max(width, this.props.minWidth);
|
||||
if (this.width === width) {
|
||||
return;
|
||||
}
|
||||
this.width = width;
|
||||
const height = Math.max(e.nativeEvent.layout.height, this.props.minHeight);
|
||||
const borderRadius = height / 2;
|
||||
this.setNativeProps({
|
||||
style: {
|
||||
width,
|
||||
height,
|
||||
borderRadius
|
||||
}
|
||||
width = Math.max(width, this.props.minWidth);
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.setNativeProps({
|
||||
style: {
|
||||
width,
|
||||
height,
|
||||
borderRadius,
|
||||
opacity: 1
|
||||
}
|
||||
});
|
||||
this.layoutReady = true;
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
renderText = () => {
|
||||
|
||||
@@ -12,25 +12,16 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import {General, WebsocketEvents} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import Drawer from 'app/components/drawer';
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {alertErrorWithFallback} from 'app/utils/general';
|
||||
import tracker from 'app/utils/time_tracker';
|
||||
|
||||
import ChannelsList from './channels_list';
|
||||
import DrawerSwiper from './drawer_swipper';
|
||||
import TeamsList from './teams_list';
|
||||
|
||||
const {
|
||||
ANDROID_TOP_LANDSCAPE,
|
||||
ANDROID_TOP_PORTRAIT,
|
||||
IOS_TOP_LANDSCAPE,
|
||||
IOS_TOP_PORTRAIT
|
||||
} = ViewTypes;
|
||||
import {General, WebsocketEvents} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
const DRAWER_INITIAL_OFFSET = 40;
|
||||
const DRAWER_LANDSCAPE_OFFSET = 150;
|
||||
|
||||
@@ -39,7 +30,7 @@ export default class ChannelDrawer extends Component {
|
||||
actions: PropTypes.shape({
|
||||
getTeams: PropTypes.func.isRequired,
|
||||
handleSelectChannel: PropTypes.func.isRequired,
|
||||
markChannelAsViewed: PropTypes.func.isRequired,
|
||||
viewChannel: PropTypes.func.isRequired,
|
||||
makeDirectChannel: PropTypes.func.isRequired,
|
||||
markChannelAsRead: PropTypes.func.isRequired,
|
||||
setChannelDisplayName: PropTypes.func.isRequired,
|
||||
@@ -57,8 +48,8 @@ export default class ChannelDrawer extends Component {
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
closeHandle = null;
|
||||
openHandle = null;
|
||||
closeLeftHandle = null;
|
||||
openLeftHandle = null;
|
||||
swiperIndex = 1;
|
||||
|
||||
constructor(props) {
|
||||
@@ -78,6 +69,7 @@ export default class ChannelDrawer extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
EventEmitter.on('open_channel_drawer', this.openChannelDrawer);
|
||||
EventEmitter.on('close_channel_drawer', this.closeChannelDrawer);
|
||||
EventEmitter.on(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
|
||||
BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack);
|
||||
@@ -110,6 +102,7 @@ export default class ChannelDrawer extends Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
EventEmitter.off('open_channel_drawer', this.openChannelDrawer);
|
||||
EventEmitter.off('close_channel_drawer', this.closeChannelDrawer);
|
||||
EventEmitter.off(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
|
||||
BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack);
|
||||
@@ -137,15 +130,15 @@ export default class ChannelDrawer extends Component {
|
||||
handleDrawerClose = () => {
|
||||
this.resetDrawer();
|
||||
|
||||
if (this.closeHandle) {
|
||||
InteractionManager.clearInteractionHandle(this.closeHandle);
|
||||
this.closeHandle = null;
|
||||
if (this.closeLeftHandle) {
|
||||
InteractionManager.clearInteractionHandle(this.closeLeftHandle);
|
||||
this.closeLeftHandle = null;
|
||||
}
|
||||
};
|
||||
|
||||
handleDrawerCloseStart = () => {
|
||||
if (!this.closeHandle) {
|
||||
this.closeHandle = InteractionManager.createInteractionHandle();
|
||||
if (!this.closeLeftHandle) {
|
||||
this.closeLeftHandle = InteractionManager.createInteractionHandle();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -154,15 +147,15 @@ export default class ChannelDrawer extends Component {
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
|
||||
if (this.openHandle) {
|
||||
InteractionManager.clearInteractionHandle(this.openHandle);
|
||||
this.openHandle = null;
|
||||
if (this.openLeftHandle) {
|
||||
InteractionManager.clearInteractionHandle(this.openLeftHandle);
|
||||
this.openLeftHandle = null;
|
||||
}
|
||||
};
|
||||
|
||||
handleDrawerOpenStart = () => {
|
||||
if (!this.openHandle) {
|
||||
this.openHandle = InteractionManager.createInteractionHandle();
|
||||
if (!this.openLeftHandle) {
|
||||
this.openLeftHandle = InteractionManager.createInteractionHandle();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -210,10 +203,9 @@ export default class ChannelDrawer extends Component {
|
||||
markChannelAsRead,
|
||||
setChannelLoading,
|
||||
setChannelDisplayName,
|
||||
markChannelAsViewed
|
||||
viewChannel
|
||||
} = actions;
|
||||
|
||||
tracker.channelSwitch = Date.now();
|
||||
setChannelLoading(channel.id !== currentChannelId);
|
||||
setChannelDisplayName(channel.display_name);
|
||||
|
||||
@@ -225,7 +217,7 @@ export default class ChannelDrawer extends Component {
|
||||
// mark the channel as viewed after all the frame has flushed
|
||||
markChannelAsRead(channel.id, currentChannelId);
|
||||
if (channel.id !== currentChannelId) {
|
||||
markChannelAsViewed(currentChannelId);
|
||||
viewChannel(currentChannelId);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -370,30 +362,21 @@ export default class ChannelDrawer extends Component {
|
||||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
backgroundColor={theme.sidebarHeaderBg}
|
||||
footerColor={theme.sidebarHeaderBg}
|
||||
navigator={navigator}
|
||||
<DrawerSwiper
|
||||
ref={this.drawerSwiperRef}
|
||||
onPageSelected={this.onPageSelected}
|
||||
openDrawerOffset={openDrawerOffset}
|
||||
showTeams={showTeams}
|
||||
>
|
||||
<DrawerSwiper
|
||||
ref={this.drawerSwiperRef}
|
||||
onPageSelected={this.onPageSelected}
|
||||
openDrawerOffset={openDrawerOffset}
|
||||
showTeams={showTeams}
|
||||
>
|
||||
{lists}
|
||||
</DrawerSwiper>
|
||||
</SafeAreaView>
|
||||
{lists}
|
||||
</DrawerSwiper>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {children, isLandscape} = this.props;
|
||||
const {children} = this.props;
|
||||
const {openDrawerOffset} = this.state;
|
||||
|
||||
const androidTop = isLandscape ? ANDROID_TOP_LANDSCAPE : ANDROID_TOP_PORTRAIT;
|
||||
const iosTop = isLandscape ? IOS_TOP_LANDSCAPE : IOS_TOP_PORTRAIT;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
ref='drawer'
|
||||
@@ -404,7 +387,7 @@ export default class ChannelDrawer extends Component {
|
||||
captureGestures='open'
|
||||
type='static'
|
||||
acceptTap={true}
|
||||
acceptPanOnDrawer={false}
|
||||
acceptPanOnDrawer={true}
|
||||
disabled={false}
|
||||
content={this.renderContent()}
|
||||
tapToClose={true}
|
||||
@@ -419,8 +402,8 @@ export default class ChannelDrawer extends Component {
|
||||
tweenDuration={100}
|
||||
tweenHandler={this.handleDrawerTween}
|
||||
elevation={-5}
|
||||
bottomPanOffset={Platform.OS === 'ios' ? ANDROID_TOP_LANDSCAPE : IOS_TOP_PORTRAIT}
|
||||
topPanOffset={Platform.OS === 'ios' ? iosTop : androidTop}
|
||||
bottomPanOffset={Platform.OS === 'ios' ? 46 : 64}
|
||||
topPanOffset={Platform.OS === 'ios' ? 64 : 46}
|
||||
styles={{
|
||||
main: {
|
||||
shadowColor: '#000000',
|
||||
|
||||
@@ -4,41 +4,30 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Animated,
|
||||
Platform,
|
||||
TouchableHighlight,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import Badge from 'app/components/badge';
|
||||
import ChannelIcon from 'app/components/channel_icon';
|
||||
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const {View: AnimatedView} = Animated;
|
||||
|
||||
export default class ChannelItem extends PureComponent {
|
||||
static propTypes = {
|
||||
channelId: PropTypes.string.isRequired,
|
||||
currentChannelId: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
fake: PropTypes.bool,
|
||||
isMyUser: PropTypes.bool,
|
||||
isUnread: PropTypes.bool,
|
||||
mentions: PropTypes.number.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
onSelectChannel: PropTypes.func.isRequired,
|
||||
status: PropTypes.string,
|
||||
type: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
onPress = wrapWithPreventDoubleTap(() => {
|
||||
const {channelId, currentChannelId, displayName, fake, onSelectChannel, type} = this.props;
|
||||
requestAnimationFrame(() => {
|
||||
@@ -46,36 +35,11 @@ export default class ChannelItem extends PureComponent {
|
||||
});
|
||||
});
|
||||
|
||||
onPreview = () => {
|
||||
const {channelId, navigator} = this.props;
|
||||
if (Platform.OS === 'ios' && navigator && this.previewRef) {
|
||||
const {intl} = this.context;
|
||||
|
||||
navigator.push({
|
||||
screen: 'ChannelPeek',
|
||||
previewCommit: false,
|
||||
previewView: this.previewRef,
|
||||
previewActions: [{
|
||||
id: 'action-mark-as-read',
|
||||
title: intl.formatMessage({id: 'mobile.channel.markAsRead', defaultMessage: 'Mark As Read'})
|
||||
}],
|
||||
passProps: {
|
||||
channelId
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
setPreviewRef = (ref) => {
|
||||
this.previewRef = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
channelId,
|
||||
currentChannelId,
|
||||
displayName,
|
||||
isMyUser,
|
||||
isUnread,
|
||||
mentions,
|
||||
status,
|
||||
@@ -83,16 +47,6 @@ export default class ChannelItem extends PureComponent {
|
||||
type
|
||||
} = this.props;
|
||||
|
||||
const {intl} = this.context;
|
||||
|
||||
let channelDisplayName = displayName;
|
||||
if (isMyUser) {
|
||||
channelDisplayName = intl.formatMessage({
|
||||
id: 'channel_header.directchannel.you',
|
||||
defaultMessage: '{displayName} (you)'
|
||||
}, {displayname: displayName});
|
||||
}
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const isActive = channelId === currentChannelId;
|
||||
|
||||
@@ -139,28 +93,25 @@ export default class ChannelItem extends PureComponent {
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatedView ref={this.setPreviewRef}>
|
||||
<TouchableHighlight
|
||||
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
|
||||
onPress={this.onPress}
|
||||
onLongPress={this.onPreview}
|
||||
>
|
||||
<View style={style.container}>
|
||||
{extraBorder}
|
||||
<View style={[style.item, extraItemStyle]}>
|
||||
{icon}
|
||||
<Text
|
||||
style={[style.text, extraTextStyle]}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{channelDisplayName}
|
||||
</Text>
|
||||
{badge}
|
||||
</View>
|
||||
<TouchableHighlight
|
||||
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<View style={style.container}>
|
||||
{extraBorder}
|
||||
<View style={[style.item, extraItemStyle]}>
|
||||
{icon}
|
||||
<Text
|
||||
style={[style.text, extraTextStyle]}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{displayName}
|
||||
</Text>
|
||||
{badge}
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
</AnimatedView>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {getCurrentChannelId, makeGetChannel, getMyChannelMember} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import ChannelItem from './channel_item';
|
||||
|
||||
@@ -20,17 +18,10 @@ function makeMapStateToProps() {
|
||||
member = getMyChannelMember(state, ownProps.channelId);
|
||||
}
|
||||
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
let isMyUser = false;
|
||||
if (channel.type === General.DM_CHANNEL && channel.teammate_id) {
|
||||
isMyUser = channel.teammate_id === currentUserId;
|
||||
}
|
||||
|
||||
return {
|
||||
currentChannelId: getCurrentChannelId(state),
|
||||
displayName: channel.display_name,
|
||||
fake: channel.fake,
|
||||
isMyUser,
|
||||
mentions: member ? member.mention_count : 0,
|
||||
status: channel.status,
|
||||
theme: getTheme(state),
|
||||
|
||||
@@ -5,21 +5,21 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
TouchableHighlight,
|
||||
View
|
||||
} from 'react-native';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import SearchBar from 'app/components/search_bar';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import FilteredList from './filtered_list';
|
||||
import List from './list';
|
||||
import SwitchTeamsButton from './switch_teams_button';
|
||||
|
||||
const {ANDROID_TOP_PORTRAIT} = ViewTypes;
|
||||
|
||||
class ChannelsList extends React.PureComponent {
|
||||
static propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
@@ -34,7 +34,7 @@ class ChannelsList extends React.PureComponent {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.firstUnreadChannel = null;
|
||||
this.state = {
|
||||
searching: false,
|
||||
term: ''
|
||||
@@ -57,6 +57,30 @@ class ChannelsList extends React.PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
openSettingsModal = wrapWithPreventDoubleTap(() => {
|
||||
const {intl, navigator, theme} = this.props;
|
||||
|
||||
navigator.showModal({
|
||||
screen: 'Settings',
|
||||
title: intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}),
|
||||
animationType: 'slide-up',
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
navBarBackgroundColor: theme.sidebarHeaderBg,
|
||||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg
|
||||
},
|
||||
navigatorButtons: {
|
||||
leftButtons: [{
|
||||
id: 'close-settings',
|
||||
icon: this.closeButton
|
||||
}]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onSearch = (term) => {
|
||||
this.setState({term});
|
||||
};
|
||||
@@ -83,6 +107,7 @@ class ChannelsList extends React.PureComponent {
|
||||
const {searching, term} = this.state;
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
let settings;
|
||||
let list;
|
||||
if (searching) {
|
||||
list = (
|
||||
@@ -93,6 +118,19 @@ class ChannelsList extends React.PureComponent {
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
settings = (
|
||||
<TouchableHighlight
|
||||
style={styles.settingsContainer}
|
||||
onPress={this.openSettingsModal}
|
||||
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
|
||||
>
|
||||
<AwesomeIcon
|
||||
name='cog'
|
||||
style={styles.settings}
|
||||
/>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
|
||||
list = (
|
||||
<List
|
||||
navigator={navigator}
|
||||
@@ -102,13 +140,6 @@ class ChannelsList extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
const searchBarInput = {
|
||||
backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.2),
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
fontSize: 15,
|
||||
lineHeight: 66
|
||||
};
|
||||
|
||||
const title = (
|
||||
<View style={styles.searchContainer}>
|
||||
<SearchBar
|
||||
@@ -117,7 +148,12 @@ class ChannelsList extends React.PureComponent {
|
||||
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
backgroundColor='transparent'
|
||||
inputHeight={33}
|
||||
inputStyle={searchBarInput}
|
||||
inputStyle={{
|
||||
backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.2),
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
fontSize: 15,
|
||||
lineHeight: 66
|
||||
}}
|
||||
placeholderTextColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
|
||||
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
|
||||
tintColorDelete={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
|
||||
@@ -145,6 +181,7 @@ class ChannelsList extends React.PureComponent {
|
||||
/>
|
||||
</View>
|
||||
{title}
|
||||
{settings}
|
||||
</View>
|
||||
</View>
|
||||
{list}
|
||||
@@ -160,7 +197,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
flex: 1
|
||||
},
|
||||
statusBar: {
|
||||
backgroundColor: theme.sidebarHeaderBg
|
||||
backgroundColor: theme.sidebarHeaderBg,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
paddingTop: 20
|
||||
}
|
||||
})
|
||||
},
|
||||
headerContainer: {
|
||||
alignItems: 'center',
|
||||
@@ -171,7 +213,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
borderBottomColor: changeOpacity(theme.sidebarHeaderTextColor, 0.10),
|
||||
...Platform.select({
|
||||
android: {
|
||||
height: ANDROID_TOP_PORTRAIT
|
||||
height: 46
|
||||
},
|
||||
ios: {
|
||||
height: 44
|
||||
@@ -189,6 +231,26 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
position: 'relative',
|
||||
top: -1
|
||||
},
|
||||
settingsContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 10,
|
||||
...Platform.select({
|
||||
android: {
|
||||
height: 46,
|
||||
marginRight: 6
|
||||
},
|
||||
ios: {
|
||||
height: 44,
|
||||
marginRight: 8
|
||||
}
|
||||
})
|
||||
},
|
||||
settings: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
fontSize: 18,
|
||||
fontWeight: '300'
|
||||
},
|
||||
titleContainer: { // These aren't used by this component, but they are passed down to the list component
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
@@ -207,7 +269,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
searchContainer: {
|
||||
flex: 1,
|
||||
paddingRight: 10,
|
||||
...Platform.select({
|
||||
android: {
|
||||
marginBottom: 1
|
||||
|
||||
@@ -20,9 +20,6 @@ import {sortChannelsByDisplayName} from 'mattermost-redux/utils/channel_utils';
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import ChannelDrawerItem from 'app/components/channel_drawer/channels_list/channel_item';
|
||||
import {ListTypes} from 'app/constants';
|
||||
|
||||
const VIEWABILITY_CONFIG = ListTypes.VISIBILITY_CONFIG_DEFAULTS;
|
||||
|
||||
class FilteredList extends Component {
|
||||
static propTypes = {
|
||||
@@ -150,7 +147,7 @@ class FilteredList extends Component {
|
||||
},
|
||||
channels: {
|
||||
builder: this.buildChannelsForSearch,
|
||||
id: 'mobile.channel_list.channels',
|
||||
id: 'sidebar.channels',
|
||||
defaultMessage: 'CHANNELS'
|
||||
},
|
||||
dms: {
|
||||
@@ -390,7 +387,10 @@ class FilteredList extends Component {
|
||||
onViewableItemsChanged={this.updateUnreadIndicators}
|
||||
keyboardDismissMode='on-drag'
|
||||
maxToRenderPerBatch={10}
|
||||
viewabilityConfig={VIEWABILITY_CONFIG}
|
||||
viewabilityConfig={{
|
||||
viewAreaCoveragePercentThreshold: 3,
|
||||
waitForInteraction: false
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -94,7 +94,7 @@ const getGroupChannelMemberDetails = createSelector(
|
||||
getGroupDetails
|
||||
);
|
||||
|
||||
function mapStateToProps(state) {
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {currentUserId} = state.entities.users;
|
||||
|
||||
const profiles = getUsers(state);
|
||||
@@ -119,7 +119,8 @@ function mapStateToProps(state) {
|
||||
searchOrder,
|
||||
pastDirectMessages: pastDirectMessages(state),
|
||||
restrictDms,
|
||||
theme: getTheme(state)
|
||||
theme: getTheme(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
TouchableHighlight,
|
||||
View
|
||||
} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
@@ -18,20 +18,15 @@ import {debounce} from 'mattermost-redux/actions/helpers';
|
||||
|
||||
import ChannelItem from 'app/components/channel_drawer/channels_list/channel_item';
|
||||
import UnreadIndicator from 'app/components/channel_drawer/channels_list/unread_indicator';
|
||||
import {ListTypes} from 'app/constants';
|
||||
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
const VIEWABILITY_CONFIG = {
|
||||
...ListTypes.VISIBILITY_CONFIG_DEFAULTS,
|
||||
waitForInteraction: true
|
||||
};
|
||||
|
||||
export default class List extends PureComponent {
|
||||
class List extends PureComponent {
|
||||
static propTypes = {
|
||||
canCreatePrivateChannels: PropTypes.bool.isRequired,
|
||||
directChannelIds: PropTypes.array.isRequired,
|
||||
favoriteChannelIds: PropTypes.array.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
onSelectChannel: PropTypes.func.isRequired,
|
||||
publicChannelIds: PropTypes.array.isRequired,
|
||||
@@ -41,10 +36,6 @@ export default class List extends PureComponent {
|
||||
unreadChannelIds: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -78,10 +69,10 @@ export default class List extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.sections !== this.state.sections && this.refs.list._wrapperListRef.getListRef()._viewabilityHelper) { //eslint-disable-line
|
||||
if (prevState.sections !== this.state.sections && this.refs.list) {
|
||||
this.refs.list.recordInteraction();
|
||||
this.updateUnreadIndicators({
|
||||
viewableItems: Array.from(this.refs.list._wrapperListRef.getListRef()._viewabilityHelper._viewableItems.values()) //eslint-disable-line
|
||||
viewableItems: Array.from(this.refs.list._wrapperListRef._listRef._viewabilityHelper._viewableItems.values()) //eslint-disable-line
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -149,8 +140,7 @@ export default class List extends PureComponent {
|
||||
};
|
||||
|
||||
goToCreatePrivateChannel = wrapWithPreventDoubleTap(() => {
|
||||
const {navigator, theme} = this.props;
|
||||
const {intl} = this.context;
|
||||
const {intl, navigator, theme} = this.props;
|
||||
|
||||
navigator.showModal({
|
||||
screen: 'CreateChannel',
|
||||
@@ -172,8 +162,7 @@ export default class List extends PureComponent {
|
||||
});
|
||||
|
||||
goToDirectMessages = wrapWithPreventDoubleTap(() => {
|
||||
const {navigator, theme} = this.props;
|
||||
const {intl} = this.context;
|
||||
const {intl, navigator, theme} = this.props;
|
||||
|
||||
navigator.showModal({
|
||||
screen: 'MoreDirectMessages',
|
||||
@@ -197,8 +186,7 @@ export default class List extends PureComponent {
|
||||
});
|
||||
|
||||
goToMoreChannels = wrapWithPreventDoubleTap(() => {
|
||||
const {navigator, theme} = this.props;
|
||||
const {intl} = this.context;
|
||||
const {intl, navigator, theme} = this.props;
|
||||
|
||||
navigator.showModal({
|
||||
screen: 'MoreChannels',
|
||||
@@ -257,7 +245,6 @@ export default class List extends PureComponent {
|
||||
return (
|
||||
<ChannelItem
|
||||
channelId={item}
|
||||
navigator={this.props.navigator}
|
||||
onSelectChannel={this.onSelectChannel}
|
||||
/>
|
||||
);
|
||||
@@ -268,15 +255,13 @@ export default class List extends PureComponent {
|
||||
<ChannelItem
|
||||
channelId={item}
|
||||
isUnread={true}
|
||||
navigator={this.props.navigator}
|
||||
onSelectChannel={this.onSelectChannel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderSectionHeader = ({section}) => {
|
||||
const {styles} = this.props;
|
||||
const {intl} = this.context;
|
||||
const {intl, styles} = this.props;
|
||||
const {
|
||||
action,
|
||||
bottomSeparator,
|
||||
@@ -301,7 +286,7 @@ export default class List extends PureComponent {
|
||||
|
||||
scrollToTop = () => {
|
||||
if (this.refs.list) {
|
||||
this.refs.list._wrapperListRef.getListRef().scrollToOffset({ //eslint-disable-line no-underscore-dangle
|
||||
this.refs.list._wrapperListRef._listRef.scrollToOffset({ //eslint-disable-line no-underscore-dangle
|
||||
x: 0,
|
||||
y: 0,
|
||||
animated: true
|
||||
@@ -346,7 +331,10 @@ export default class List extends PureComponent {
|
||||
keyboardDismissMode='on-drag'
|
||||
maxToRenderPerBatch={10}
|
||||
stickySectionHeadersEnabled={false}
|
||||
viewabilityConfig={VIEWABILITY_CONFIG}
|
||||
viewabilityConfig={{
|
||||
viewAreaCoveragePercentThreshold: 3,
|
||||
waitForInteraction: true
|
||||
}}
|
||||
/>
|
||||
<UnreadIndicator
|
||||
show={showIndicator}
|
||||
@@ -358,3 +346,5 @@ export default class List extends PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(List);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import Svg, {
|
||||
G,
|
||||
Path
|
||||
} from 'react-native-svg';
|
||||
|
||||
export default class AboveIcon extends PureComponent {
|
||||
static propTypes = {
|
||||
width: PropTypes.number.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
color: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const {color, height, width} = this.props;
|
||||
|
||||
return (
|
||||
<View style={[style.container, {height, width}]}>
|
||||
<Svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox='0 0 10 10'
|
||||
>
|
||||
<G transform='matrix(1,0,0,1,-20,-18)'>
|
||||
<G transform='matrix(0.0330723,0,0,0.0322634,15.8132,12.3164)'>
|
||||
<Path
|
||||
d='M245.803,377.493C245.803,377.493 204.794,336.485 179.398,311.088C168.55,300.24 150.962,300.24 140.114,311.088C138.327,312.875 136.517,314.686 134.73,316.473C123.882,327.321 123.882,344.908 134.73,355.756C167.972,388.998 233.949,454.975 256.949,477.975C262.158,483.184 269.223,486.111 276.591,486.111C277.38,486.111 278.176,486.111 278.965,486.111C286.332,486.111 293.397,483.184 298.607,477.975C321.607,454.975 387.584,388.998 420.826,355.756C431.674,344.908 431.674,327.321 420.826,316.473C419.039,314.686 417.228,312.875 415.441,311.088C404.593,300.24 387.005,300.24 376.158,311.088C350.761,336.485 309.753,377.493 309.753,377.493C309.753,377.493 309.753,279.687 309.753,203.94C309.753,196.573 306.826,189.508 301.617,184.298C296.408,179.089 289.342,176.162 281.975,176.162C279.191,176.162 276.364,176.162 273.58,176.162C266.213,176.162 259.148,179.089 253.939,184.298C248.729,189.508 245.803,196.573 245.803,203.94L245.803,377.493Z'
|
||||
fill={color}
|
||||
/>
|
||||
</G>
|
||||
</G>
|
||||
</Svg>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'flex-start',
|
||||
transform: [{rotate: '180deg'}]
|
||||
}
|
||||
});
|
||||
@@ -8,11 +8,12 @@ import {
|
||||
View,
|
||||
ViewPropTypes
|
||||
} from 'react-native';
|
||||
import IonIcon from 'react-native-vector-icons/Ionicons';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import AboveIcon from './above_icon';
|
||||
|
||||
export default class UnreadIndicator extends PureComponent {
|
||||
static propTypes = {
|
||||
show: PropTypes.bool,
|
||||
@@ -44,11 +45,10 @@ export default class UnreadIndicator extends PureComponent {
|
||||
id='sidebar.unreads'
|
||||
defaultMessage='More unreads'
|
||||
/>
|
||||
<IonIcon
|
||||
size={14}
|
||||
name='md-arrow-round-up'
|
||||
<AboveIcon
|
||||
width={12}
|
||||
height={12}
|
||||
color={theme.mentionColor}
|
||||
style={style.arrow}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
@@ -75,10 +75,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
paddingHorizontal: 4,
|
||||
textAlign: 'center',
|
||||
textAlignVertical: 'center'
|
||||
},
|
||||
arrow: {
|
||||
position: 'relative',
|
||||
bottom: -1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {joinChannel, markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
|
||||
import {joinChannel, viewChannel, markChannelAsRead} from 'mattermost-redux/actions/channels';
|
||||
import {getTeams} from 'mattermost-redux/actions/teams';
|
||||
import {getCurrentTeamId, getMyTeamsCount} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
@@ -34,7 +34,7 @@ function mapDispatchToProps(dispatch) {
|
||||
getTeams,
|
||||
handleSelectChannel,
|
||||
joinChannel,
|
||||
markChannelAsViewed,
|
||||
viewChannel,
|
||||
makeDirectChannel,
|
||||
markChannelAsRead,
|
||||
setChannelDisplayName,
|
||||
@@ -43,4 +43,4 @@ function mapDispatchToProps(dispatch) {
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps, null, {withRef: true})(ChannelDrawer);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChannelDrawer);
|
||||
|
||||
@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
FlatList,
|
||||
Platform,
|
||||
StatusBar,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
View
|
||||
@@ -15,19 +14,11 @@ import {injectIntl, intlShape} from 'react-intl';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {ListTypes, ViewTypes} from 'app/constants';
|
||||
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import tracker from 'app/utils/time_tracker';
|
||||
|
||||
import TeamsListItem from './teams_list_item';
|
||||
|
||||
const {ANDROID_TOP_PORTRAIT} = ViewTypes;
|
||||
const VIEWABILITY_CONFIG = {
|
||||
...ListTypes.VISIBILITY_CONFIG_DEFAULTS,
|
||||
waitForInteraction: true
|
||||
};
|
||||
|
||||
class TeamsList extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
@@ -52,11 +43,9 @@ class TeamsList extends PureComponent {
|
||||
}
|
||||
|
||||
selectTeam = (teamId) => {
|
||||
StatusBar.setHidden(false, 'slide');
|
||||
requestAnimationFrame(() => {
|
||||
const {actions, closeChannelDrawer, currentTeamId} = this.props;
|
||||
if (teamId !== currentTeamId) {
|
||||
tracker.teamSwitch = Date.now();
|
||||
actions.handleTeamChange(teamId);
|
||||
}
|
||||
|
||||
@@ -142,7 +131,10 @@ class TeamsList extends PureComponent {
|
||||
data={teamIds}
|
||||
renderItem={this.renderItem}
|
||||
keyExtractor={this.keyExtractor}
|
||||
viewabilityConfig={VIEWABILITY_CONFIG}
|
||||
viewabilityConfig={{
|
||||
viewAreaCoveragePercentThreshold: 3,
|
||||
waitForInteraction: false
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -156,7 +148,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
flex: 1
|
||||
},
|
||||
statusBar: {
|
||||
backgroundColor: theme.sidebarHeaderBg
|
||||
backgroundColor: theme.sidebarHeaderBg,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
paddingTop: 20
|
||||
}
|
||||
})
|
||||
},
|
||||
headerContainer: {
|
||||
alignItems: 'center',
|
||||
@@ -166,7 +163,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
borderBottomColor: changeOpacity(theme.sidebarHeaderTextColor, 0.10),
|
||||
...Platform.select({
|
||||
android: {
|
||||
height: ANDROID_TOP_PORTRAIT
|
||||
height: 46
|
||||
},
|
||||
ios: {
|
||||
height: 44
|
||||
@@ -186,7 +183,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
width: 50,
|
||||
...Platform.select({
|
||||
android: {
|
||||
height: ANDROID_TOP_PORTRAIT
|
||||
height: 46
|
||||
},
|
||||
ios: {
|
||||
height: 44
|
||||
|
||||
@@ -11,21 +11,18 @@ import {removeProtocol} from 'app/utils/url';
|
||||
|
||||
import TeamsListItem from './teams_list_item.js';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const team = getTeam(state, ownProps.teamId);
|
||||
const getMentionCount = makeGetBadgeCountForTeamId();
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const team = getTeam(state, ownProps.teamId);
|
||||
|
||||
return {
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
currentUrl: removeProtocol(getCurrentUrl(state)),
|
||||
displayName: team.display_name,
|
||||
mentionCount: getMentionCount(state, ownProps.teamId),
|
||||
name: team.name,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
return {
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
currentUrl: removeProtocol(getCurrentUrl(state)),
|
||||
displayName: team.display_name,
|
||||
mentionCount: getMentionCount(state, ownProps.teamId),
|
||||
name: team.name,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(TeamsListItem);
|
||||
export default connect(mapStateToProps)(TeamsListItem);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import {AwayAvatar, DndAvatar, OfflineAvatar, OnlineAvatar} from 'app/components/status_icons';
|
||||
import {OnlineStatus, AwayStatus, OfflineStatus} from 'app/components/status_icons';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
@@ -90,43 +90,30 @@ export default class ChannelIcon extends React.PureComponent {
|
||||
</View>
|
||||
);
|
||||
} else if (type === General.DM_CHANNEL) {
|
||||
switch (status) {
|
||||
case General.AWAY:
|
||||
if (status === General.ONLINE) {
|
||||
icon = (
|
||||
<AwayAvatar
|
||||
width={size}
|
||||
height={size}
|
||||
color={theme.awayIndicator}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case General.DND:
|
||||
icon = (
|
||||
<DndAvatar
|
||||
width={size}
|
||||
height={size}
|
||||
color={theme.dndIndicator}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case General.ONLINE:
|
||||
icon = (
|
||||
<OnlineAvatar
|
||||
<OnlineStatus
|
||||
width={size}
|
||||
height={size}
|
||||
color={theme.onlineIndicator}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
} else if (status === General.AWAY) {
|
||||
icon = (
|
||||
<OfflineAvatar
|
||||
<AwayStatus
|
||||
width={size}
|
||||
height={size}
|
||||
color={theme.awayIndicator}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
icon = (
|
||||
<OfflineStatus
|
||||
width={size}
|
||||
height={size}
|
||||
color={offlineColor}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ class ChannelIntro extends PureComponent {
|
||||
size={64}
|
||||
statusBorderWidth={2}
|
||||
statusSize={25}
|
||||
statusIconSize={15}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
));
|
||||
|
||||
@@ -2,79 +2,41 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {General, RequestStatus} from 'mattermost-redux/constants';
|
||||
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId, getUser, makeGetProfilesInChannel} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentUser, getProfilesInCurrentChannel} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import ChannelIntro from './channel_intro';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getChannel = makeGetChannel();
|
||||
const getProfilesInChannel = makeGetProfilesInChannel();
|
||||
function mapStateToProps(state) {
|
||||
const currentChannel = getCurrentChannel(state) || {};
|
||||
const currentUser = getCurrentUser(state) || {};
|
||||
const {status: getPostsRequestStatus} = state.requests.posts.getPosts;
|
||||
|
||||
const getOtherUserIdForDm = createSelector(
|
||||
(state, channel) => channel,
|
||||
getCurrentUserId,
|
||||
(channel, currentUserId) => {
|
||||
if (!channel) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return channel.name.split('__').find((m) => m !== currentUserId) || currentUserId;
|
||||
let currentChannelMembers = [];
|
||||
if (currentChannel.type === General.DM_CHANNEL) {
|
||||
const otherChannelMember = currentChannel.name.split('__').find((m) => m !== currentUser.id);
|
||||
const otherProfile = state.entities.users.profiles[otherChannelMember];
|
||||
if (otherProfile) {
|
||||
currentChannelMembers.push(otherProfile);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
currentChannelMembers = getProfilesInCurrentChannel(state) || [];
|
||||
currentChannelMembers = currentChannelMembers.filter((m) => m.id !== currentUser.id);
|
||||
}
|
||||
|
||||
const getChannelMembers = createSelector(
|
||||
getCurrentUserId,
|
||||
(state, channel) => getProfilesInChannel(state, channel.id),
|
||||
(currentUserId, profilesInChannel) => {
|
||||
const currentChannelMembers = profilesInChannel || [];
|
||||
return currentChannelMembers.filter((m) => m.id !== currentUserId);
|
||||
}
|
||||
);
|
||||
const creator = currentChannel.creator_id === currentUser.id ? currentUser : state.entities.users.profiles[currentChannel.creator_id];
|
||||
const postsInChannel = state.entities.posts.postsInChannel[currentChannel.id] || [];
|
||||
|
||||
const getChannelMembersForDm = createSelector(
|
||||
(state, channel) => getUser(state, getOtherUserIdForDm(state, channel)),
|
||||
(otherUser) => {
|
||||
if (!otherUser) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [otherUser];
|
||||
}
|
||||
);
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const currentChannel = getChannel(state, {id: ownProps.channelId}) || {};
|
||||
const {status: getPostsRequestStatus} = state.requests.posts.getPosts;
|
||||
|
||||
let currentChannelMembers;
|
||||
let creator;
|
||||
let postsInChannel;
|
||||
|
||||
if (currentChannel) {
|
||||
if (currentChannel.type === General.DM_CHANNEL) {
|
||||
currentChannelMembers = getChannelMembersForDm(state, currentChannel);
|
||||
} else {
|
||||
currentChannelMembers = getChannelMembers(state, currentChannel);
|
||||
}
|
||||
|
||||
creator = getUser(state, currentChannel.creator_id);
|
||||
postsInChannel = state.entities.posts.postsInChannel[currentChannel.Id];
|
||||
}
|
||||
|
||||
return {
|
||||
creator,
|
||||
currentChannel,
|
||||
currentChannelMembers,
|
||||
isLoadingPosts: (!postsInChannel || postsInChannel.length === 0) && getPostsRequestStatus === RequestStatus.STARTED,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
return {
|
||||
creator,
|
||||
currentChannel,
|
||||
currentChannelMembers,
|
||||
isLoadingPosts: !postsInChannel.length && getPostsRequestStatus === RequestStatus.STARTED,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(ChannelIntro);
|
||||
export default connect(mapStateToProps)(ChannelIntro);
|
||||
|
||||
@@ -10,9 +10,10 @@ import {handleSelectChannel, setChannelDisplayName} from 'app/actions/views/chan
|
||||
|
||||
import ChannelLink from './channel_link';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
channelsByName: getChannelsNameMapInCurrentTeam(state)
|
||||
channelsByName: getChannelsNameMapInCurrentTeam(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,7 @@ import {
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {UpgradeTypes} from 'app/constants/view';
|
||||
@@ -23,7 +21,7 @@ const {View: AnimatedView} = Animated;
|
||||
|
||||
const UPDATE_TIMEOUT = 60000;
|
||||
|
||||
export default class ClientUpgradeListener extends PureComponent {
|
||||
class ClientUpgradeListener extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
logError: PropTypes.func.isRequired,
|
||||
@@ -32,7 +30,7 @@ export default class ClientUpgradeListener extends PureComponent {
|
||||
currentVersion: PropTypes.string,
|
||||
downloadLink: PropTypes.string,
|
||||
forceUpgrade: PropTypes.bool,
|
||||
isLandscape: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
lastUpgradeCheck: PropTypes.number,
|
||||
latestVersion: PropTypes.string,
|
||||
minVersion: PropTypes.string,
|
||||
@@ -40,28 +38,14 @@ export default class ClientUpgradeListener extends PureComponent {
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.isX = DeviceInfo.getModel() === 'iPhone X';
|
||||
|
||||
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => {
|
||||
this.closeButton = source;
|
||||
});
|
||||
|
||||
this.state = {
|
||||
top: new Animated.Value(-100)
|
||||
};
|
||||
state = {
|
||||
top: new Animated.Value(-100)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {forceUpgrade, isLandscape, lastUpgradeCheck, latestVersion, minVersion} = this.props;
|
||||
const {forceUpgrade, lastUpgradeCheck, latestVersion, minVersion} = this.props;
|
||||
if (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT) {
|
||||
this.checkUpgrade(minVersion, latestVersion, isLandscape);
|
||||
this.checkUpgrade(minVersion, latestVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,54 +55,40 @@ export default class ClientUpgradeListener extends PureComponent {
|
||||
|
||||
const versionMismatch = latestVersion !== nextLatestVersion || minVersion !== nextMinVersion;
|
||||
if (versionMismatch && (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT)) {
|
||||
this.checkUpgrade(minVersion, latestVersion, nextProps.isLandscape);
|
||||
} else if (this.props.isLandscape !== nextProps.isLandscape &&
|
||||
this.state.upgradeType !== UpgradeTypes.NO_UPGRADE && this.isX) {
|
||||
const newTop = nextProps.isLandscape ? 45 : 100;
|
||||
this.setState({top: new Animated.Value(newTop)});
|
||||
this.checkUpgrade(minVersion, latestVersion);
|
||||
}
|
||||
}
|
||||
|
||||
checkUpgrade = (minVersion, latestVersion, isLandscape) => {
|
||||
checkUpgrade = (minVersion, latestVersion) => {
|
||||
const {actions, currentVersion} = this.props;
|
||||
|
||||
const upgradeType = checkUpgradeType(currentVersion, minVersion, latestVersion, actions.logError);
|
||||
|
||||
this.setState({upgradeType});
|
||||
|
||||
if (upgradeType === UpgradeTypes.NO_UPGRADE) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.toggleUpgradeMessage(true, isLandscape);
|
||||
}, 500);
|
||||
this.setState({upgradeType});
|
||||
|
||||
setTimeout(this.toggleUpgradeMessage, 500);
|
||||
|
||||
actions.setLastUpgradeCheck();
|
||||
};
|
||||
}
|
||||
|
||||
toggleUpgradeMessage = (show = true, isLandscape) => {
|
||||
let toValue = -100;
|
||||
if (show) {
|
||||
if (this.isX && isLandscape) {
|
||||
toValue = 45;
|
||||
} else {
|
||||
toValue = this.isX ? 100 : 75;
|
||||
}
|
||||
}
|
||||
toggleUpgradeMessage = (show = true) => {
|
||||
const toValue = show ? 75 : -100;
|
||||
Animated.timing(this.state.top, {
|
||||
toValue,
|
||||
duration: 300
|
||||
}).start();
|
||||
};
|
||||
}
|
||||
|
||||
handleDismiss = () => {
|
||||
this.toggleUpgradeMessage(false);
|
||||
};
|
||||
}
|
||||
|
||||
handleDownload = () => {
|
||||
const {downloadLink} = this.props;
|
||||
const {intl} = this.context;
|
||||
const {downloadLink, intl} = this.props;
|
||||
|
||||
Linking.canOpenURL(downloadLink).then((supported) => {
|
||||
if (supported) {
|
||||
@@ -140,25 +110,17 @@ export default class ClientUpgradeListener extends PureComponent {
|
||||
});
|
||||
|
||||
this.toggleUpgradeMessage(false);
|
||||
};
|
||||
}
|
||||
|
||||
handleLearnMore = () => {
|
||||
const {intl} = this.context;
|
||||
this.props.navigator.dismissModal({animationType: 'none'});
|
||||
this.props.navigator.dismissAllModals({animationType: 'none'});
|
||||
|
||||
this.props.navigator.showModal({
|
||||
screen: 'ClientUpgrade',
|
||||
title: intl.formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Upgrade App'}),
|
||||
navigatorStyle: {
|
||||
navBarHidden: false,
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false
|
||||
},
|
||||
navigatorButtons: {
|
||||
leftButtons: [{
|
||||
id: 'close-upgrade',
|
||||
icon: this.closeButton
|
||||
}]
|
||||
navBarHidden: true,
|
||||
statusBarHidden: true,
|
||||
statusBarHideWithNavBar: true
|
||||
},
|
||||
passProps: {
|
||||
upgradeType: this.state.upgradeType
|
||||
@@ -166,13 +128,9 @@ export default class ClientUpgradeListener extends PureComponent {
|
||||
});
|
||||
|
||||
this.toggleUpgradeMessage(false);
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.upgradeType === UpgradeTypes.NO_UPGRADE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {forceUpgrade, theme} = this.props;
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
@@ -269,3 +227,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(ClientUpgradeListener);
|
||||
|
||||
@@ -5,7 +5,6 @@ import {logError} from 'mattermost-redux/actions/errors';
|
||||
|
||||
import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade';
|
||||
import getClientUpgrade from 'app/selectors/client_upgrade';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import ClientUpgradeListener from './client_upgrade_listener';
|
||||
@@ -17,7 +16,6 @@ function mapStateToProps(state) {
|
||||
currentVersion,
|
||||
downloadLink,
|
||||
forceUpgrade,
|
||||
isLandscape: isLandscape(state),
|
||||
lastUpgradeCheck: state.views.clientUpgrade.lastUpdateCheck,
|
||||
latestVersion,
|
||||
minVersion,
|
||||
|
||||
@@ -15,7 +15,8 @@ function makeMapStateToProps() {
|
||||
return (state, ownProps) => {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
channel: getChannel(state, ownProps)
|
||||
channel: getChannel(state, ownProps),
|
||||
...ownProps
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import UserListRow from './user_list_row';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
isMyUser: getCurrentUserId(state) === ownProps.id,
|
||||
theme: getTheme(state),
|
||||
user: getUser(state, ownProps.id),
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state)
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Text,
|
||||
View
|
||||
@@ -18,63 +17,36 @@ import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
export default class UserListRow extends React.PureComponent {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
isMyUser: PropTypes.bool.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
teammateNameDisplay: PropTypes.string.isRequired,
|
||||
...CustomListRow.propTypes
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
onPress = () => {
|
||||
if (this.props.onPress) {
|
||||
this.props.onPress(this.props.id);
|
||||
}
|
||||
this.props.onPress(this.props.id);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {
|
||||
enabled,
|
||||
isMyUser,
|
||||
selectable,
|
||||
selected,
|
||||
teammateNameDisplay,
|
||||
theme,
|
||||
user
|
||||
} = this.props;
|
||||
|
||||
const {id, username} = user;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
let usernameDisplay = `(@${username})`;
|
||||
if (isMyUser) {
|
||||
usernameDisplay = formatMessage({
|
||||
id: 'mobile.more_dms.you',
|
||||
defaultMessage: '(@{username} - you)'
|
||||
}, {username});
|
||||
}
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
return (
|
||||
<CustomListRow
|
||||
id={id}
|
||||
theme={theme}
|
||||
onPress={this.onPress}
|
||||
enabled={enabled}
|
||||
selectable={selectable}
|
||||
selected={selected}
|
||||
id={this.props.id}
|
||||
theme={this.props.theme}
|
||||
onPress={this.props.onPress ? this.onPress : null}
|
||||
enabled={this.props.enabled}
|
||||
selectable={this.props.selectable}
|
||||
selected={this.props.selected}
|
||||
>
|
||||
<ProfilePicture
|
||||
userId={id}
|
||||
userId={this.props.user.id}
|
||||
size={32}
|
||||
/>
|
||||
<View style={style.textContainer}>
|
||||
<View>
|
||||
<Text style={style.displayName}>
|
||||
{displayUsername(user, teammateNameDisplay)}
|
||||
{displayUsername(this.props.user, this.props.teammateNameDisplay)}
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
@@ -83,7 +55,7 @@ export default class UserListRow extends React.PureComponent {
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{usernameDisplay}
|
||||
{`(@${this.props.user.username})`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -88,11 +88,13 @@ export default class CustomSectionList extends React.PureComponent {
|
||||
initialNumToRender: PropTypes.number
|
||||
};
|
||||
|
||||
static defaultKeyExtractor = (item) => {
|
||||
return item.id;
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
showNoResults: true,
|
||||
keyExtractor: (item) => {
|
||||
return item.id;
|
||||
},
|
||||
keyExtractor: CustomSectionList.defaultKeyExtractor,
|
||||
onListEndReached: () => true,
|
||||
onListEndReachedThreshold: 50,
|
||||
loadingText: null,
|
||||
|
||||
@@ -1,427 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
Text,
|
||||
findNodeHandle
|
||||
} from 'react-native';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import ErrorText from 'app/components/error_text';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import Loading from 'app/components/loading';
|
||||
import StatusBar from 'app/components/status_bar';
|
||||
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {getShortenedURL} from 'app/utils/url';
|
||||
|
||||
export default class EditChannelInfo extends PureComponent {
|
||||
static propTypes = {
|
||||
navigator: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
channelType: PropTypes.string,
|
||||
enableRightButton: PropTypes.func,
|
||||
saving: PropTypes.bool.isRequired,
|
||||
editing: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
displayName: PropTypes.string,
|
||||
currentTeamUrl: PropTypes.string,
|
||||
channelURL: PropTypes.string,
|
||||
purpose: PropTypes.string,
|
||||
header: PropTypes.string,
|
||||
onDisplayNameChange: PropTypes.func,
|
||||
onChannelURLChange: PropTypes.func,
|
||||
onPurposeChange: PropTypes.func,
|
||||
onHeaderChange: PropTypes.func,
|
||||
oldDisplayName: PropTypes.string,
|
||||
oldChannelURL: PropTypes.string,
|
||||
oldHeader: PropTypes.string,
|
||||
oldPurpose: PropTypes.string
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
editing: false
|
||||
};
|
||||
|
||||
blur = () => {
|
||||
if (this.nameInput) {
|
||||
this.nameInput.refs.wrappedInstance.blur();
|
||||
}
|
||||
|
||||
// TODO: uncomment below once the channel URL field is added
|
||||
// if (this.urlInput) {
|
||||
// this.urlInput.refs.wrappedInstance.blur();
|
||||
// }
|
||||
if (this.purposeInput) {
|
||||
this.purposeInput.refs.wrappedInstance.blur();
|
||||
}
|
||||
if (this.headerInput) {
|
||||
this.headerInput.refs.wrappedInstance.blur();
|
||||
}
|
||||
if (this.scroll) {
|
||||
this.scroll.scrollToPosition(0, 0, true);
|
||||
}
|
||||
};
|
||||
|
||||
channelNameRef = (ref) => {
|
||||
this.nameInput = ref;
|
||||
};
|
||||
|
||||
channelURLRef = (ref) => {
|
||||
this.urlInput = ref;
|
||||
};
|
||||
|
||||
channelPurposeRef = (ref) => {
|
||||
this.purposeInput = ref;
|
||||
};
|
||||
|
||||
channelHeaderRef = (ref) => {
|
||||
this.headerInput = ref;
|
||||
};
|
||||
|
||||
close = (goBack = false) => {
|
||||
EventEmitter.emit('closing-create-channel', false);
|
||||
if (goBack) {
|
||||
this.props.navigator.pop({animated: true});
|
||||
} else {
|
||||
this.props.navigator.dismissModal({
|
||||
animationType: 'slide-down'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
lastTextRef = (ref) => {
|
||||
this.lastText = ref;
|
||||
};
|
||||
|
||||
canUpdate = (displayName, channelURL, purpose, header) => {
|
||||
const {
|
||||
oldDisplayName,
|
||||
oldChannelURL,
|
||||
oldPurpose,
|
||||
oldHeader
|
||||
} = this.props;
|
||||
|
||||
return displayName !== oldDisplayName || channelURL !== oldChannelURL ||
|
||||
purpose !== oldPurpose || header !== oldHeader;
|
||||
};
|
||||
|
||||
enableRightButton = (enable = false) => {
|
||||
this.props.enableRightButton(enable);
|
||||
};
|
||||
|
||||
onDisplayNameChangeText = (displayName) => {
|
||||
const {editing, onDisplayNameChange} = this.props;
|
||||
onDisplayNameChange(displayName);
|
||||
|
||||
if (editing) {
|
||||
const {channelURL, purpose, header} = this.props;
|
||||
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
|
||||
this.enableRightButton(canUpdate);
|
||||
return;
|
||||
}
|
||||
|
||||
const displayNameExists = displayName && displayName.length >= 2;
|
||||
this.props.enableRightButton(displayNameExists);
|
||||
};
|
||||
|
||||
onDisplayURLChangeText = (channelURL) => {
|
||||
const {editing, onChannelURLChange} = this.props;
|
||||
onChannelURLChange(channelURL);
|
||||
|
||||
if (editing) {
|
||||
const {displayName, purpose, header} = this.props;
|
||||
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
|
||||
this.enableRightButton(canUpdate);
|
||||
}
|
||||
};
|
||||
|
||||
onPurposeChangeText = (purpose) => {
|
||||
const {editing, onPurposeChange} = this.props;
|
||||
onPurposeChange(purpose);
|
||||
|
||||
if (editing) {
|
||||
const {displayName, channelURL, header} = this.props;
|
||||
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
|
||||
this.enableRightButton(canUpdate);
|
||||
}
|
||||
};
|
||||
|
||||
onHeaderChangeText = (header) => {
|
||||
const {editing, onHeaderChange} = this.props;
|
||||
onHeaderChange(header);
|
||||
|
||||
if (editing) {
|
||||
const {displayName, channelURL, purpose} = this.props;
|
||||
const canUpdate = this.canUpdate(displayName, channelURL, purpose, header);
|
||||
this.enableRightButton(canUpdate);
|
||||
}
|
||||
};
|
||||
|
||||
scrollRef = (ref) => {
|
||||
this.scroll = ref;
|
||||
};
|
||||
|
||||
scrollToEnd = () => {
|
||||
if (this.scroll && this.lastText) {
|
||||
this.scroll.scrollToFocusedInput(findNodeHandle(this.lastText));
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
theme,
|
||||
editing,
|
||||
channelType,
|
||||
currentTeamUrl,
|
||||
deviceWidth,
|
||||
deviceHeight,
|
||||
displayName,
|
||||
channelURL,
|
||||
header,
|
||||
purpose
|
||||
} = this.props;
|
||||
const {error, saving} = this.props;
|
||||
const fullUrl = currentTeamUrl + '/channels';
|
||||
const shortUrl = getShortenedURL(fullUrl, 35);
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const displayHeaderOnly = channelType === General.DM_CHANNEL ||
|
||||
channelType === General.GM_CHANNEL;
|
||||
|
||||
if (saving) {
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<StatusBar/>
|
||||
<Loading/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let displayError;
|
||||
if (error) {
|
||||
displayError = (
|
||||
<View style={[style.errorContainer, {deviceWidth}]}>
|
||||
<View style={style.errorWrapper}>
|
||||
<ErrorText error={error}/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<StatusBar/>
|
||||
<KeyboardAwareScrollView
|
||||
ref={this.scrollRef}
|
||||
style={style.container}
|
||||
>
|
||||
{displayError}
|
||||
<TouchableWithoutFeedback onPress={this.blur}>
|
||||
<View style={[style.scrollView, {height: deviceHeight + (Platform.OS === 'android' ? 200 : 0)}]}>
|
||||
{!displayHeaderOnly && (
|
||||
<View>
|
||||
<View>
|
||||
<FormattedText
|
||||
style={style.title}
|
||||
id='channel_modal.name'
|
||||
defaultMessage='Name'
|
||||
/>
|
||||
</View>
|
||||
<View style={style.inputContainer}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
ref={this.channelNameRef}
|
||||
value={displayName}
|
||||
onChangeText={this.onDisplayNameChangeText}
|
||||
style={style.input}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
placeholder={{id: 'channel_modal.nameEx', defaultMessage: 'E.g.: "Bugs", "Marketing", "客户支持"'}}
|
||||
placeholderTextColor={changeOpacity('#000', 0.5)}
|
||||
underlineColorAndroid='transparent'
|
||||
disableFullscreenUI={true}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{/*TODO: Hide channel url field until it's added to CreateChannel */}
|
||||
{false && editing && !displayHeaderOnly && (
|
||||
<View>
|
||||
<View style={style.titleContainer30}>
|
||||
<FormattedText
|
||||
style={style.title}
|
||||
id='rename_channel.url'
|
||||
defaultMessage='URL'
|
||||
/>
|
||||
<Text style={style.optional}>
|
||||
{shortUrl}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={style.inputContainer}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
ref={this.channelURLRef}
|
||||
value={channelURL}
|
||||
onChangeText={this.onDisplayURLChangeText}
|
||||
style={style.input}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
placeholder={{id: 'rename_channel.handleHolder', defaultMessage: 'lowercase alphanumeric characters'}}
|
||||
placeholderTextColor={changeOpacity('#000', 0.5)}
|
||||
underlineColorAndroid='transparent'
|
||||
disableFullscreenUI={true}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
{!displayHeaderOnly && (
|
||||
<View>
|
||||
<View style={style.titleContainer30}>
|
||||
<FormattedText
|
||||
style={style.title}
|
||||
id='channel_modal.purpose'
|
||||
defaultMessage='Purpose'
|
||||
/>
|
||||
<FormattedText
|
||||
style={style.optional}
|
||||
id='channel_modal.optional'
|
||||
defaultMessage='(optional)'
|
||||
/>
|
||||
</View>
|
||||
<View style={style.inputContainer}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
ref={this.channelPurposeRef}
|
||||
value={purpose}
|
||||
onChangeText={this.onPurposeChangeText}
|
||||
style={[style.input, {height: 110}]}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
placeholder={{id: 'channel_modal.purposeEx', defaultMessage: 'E.g.: "A channel to file bugs and improvements"'}}
|
||||
placeholderTextColor={changeOpacity('#000', 0.5)}
|
||||
multiline={true}
|
||||
blurOnSubmit={false}
|
||||
textAlignVertical='top'
|
||||
underlineColorAndroid='transparent'
|
||||
disableFullscreenUI={true}
|
||||
/>
|
||||
</View>
|
||||
<View>
|
||||
<FormattedText
|
||||
style={style.helpText}
|
||||
id='channel_modal.descriptionHelp'
|
||||
defaultMessage='Describe how this channel should be used.'
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View style={style.titleContainer15}>
|
||||
<FormattedText
|
||||
style={style.title}
|
||||
id='channel_modal.header'
|
||||
defaultMessage='Header'
|
||||
/>
|
||||
<FormattedText
|
||||
style={style.optional}
|
||||
id='channel_modal.optional'
|
||||
defaultMessage='(optional)'
|
||||
/>
|
||||
</View>
|
||||
<View style={style.inputContainer}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
ref={this.channelHeaderRef}
|
||||
value={header}
|
||||
onChangeText={this.onHeaderChangeText}
|
||||
style={[style.input, {height: 110}]}
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
placeholder={{id: 'channel_modal.headerEx', defaultMessage: 'E.g.: "[Link Title](http://example.com)"'}}
|
||||
placeholderTextColor={changeOpacity('#000', 0.5)}
|
||||
multiline={true}
|
||||
blurOnSubmit={false}
|
||||
onFocus={this.scrollToEnd}
|
||||
textAlignVertical='top'
|
||||
underlineColorAndroid='transparent'
|
||||
disableFullscreenUI={true}
|
||||
/>
|
||||
</View>
|
||||
<View ref={this.lastTextRef}>
|
||||
<FormattedText
|
||||
style={style.helpText}
|
||||
id='channel_modal.headerHelp'
|
||||
defaultMessage={'Set text that will appear in the header of the channel beside the channel name. For example, include frequently used links by typing [Link Title](http://example.com).'}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</KeyboardAwareScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
|
||||
paddingTop: 10
|
||||
},
|
||||
errorContainer: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03)
|
||||
},
|
||||
errorWrapper: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
inputContainer: {
|
||||
marginTop: 10,
|
||||
backgroundColor: '#fff'
|
||||
},
|
||||
input: {
|
||||
color: '#333',
|
||||
fontSize: 14,
|
||||
height: 40,
|
||||
paddingHorizontal: 15
|
||||
},
|
||||
titleContainer30: {
|
||||
flexDirection: 'row',
|
||||
marginTop: 30
|
||||
},
|
||||
titleContainer15: {
|
||||
flexDirection: 'row',
|
||||
marginTop: 15
|
||||
},
|
||||
title: {
|
||||
fontSize: 14,
|
||||
color: theme.centerChannelColor,
|
||||
marginLeft: 15
|
||||
},
|
||||
optional: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
fontSize: 14,
|
||||
marginLeft: 5
|
||||
},
|
||||
helpText: {
|
||||
fontSize: 14,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
marginTop: 10,
|
||||
marginHorizontal: 15
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -138,7 +138,7 @@ export default class Emoji extends React.PureComponent {
|
||||
let marginTop = 0;
|
||||
if (fontSize) {
|
||||
// Center the image vertically on iOS (does nothing on Android)
|
||||
marginTop = (height - 16) / 2;
|
||||
marginTop = (height - fontSize) / 2;
|
||||
|
||||
// hack to get the vertical alignment looking better
|
||||
if (fontSize === 17) {
|
||||
|
||||
@@ -7,8 +7,9 @@ import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis'
|
||||
|
||||
import Emoji from './emoji';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
customEmojis: getCustomEmojisByName(state),
|
||||
token: state.entities.general.credentials.token
|
||||
};
|
||||
|
||||
@@ -3,41 +3,28 @@
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
FlatList,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
SectionList,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import FontAwesomeIcon from 'react-native-vector-icons/FontAwesome';
|
||||
import sectionListGetItemLayout from 'react-native-section-list-get-item-layout';
|
||||
|
||||
import Emoji from 'app/components/emoji';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import SafeAreaView from 'app/components/safe_area_view';
|
||||
import SearchBar from 'app/components/search_bar';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import EmojiPickerRow from './emoji_picker_row';
|
||||
|
||||
const EMOJI_SIZE = 30;
|
||||
const EMOJI_GUTTER = 7.5;
|
||||
const SECTION_MARGIN = 15;
|
||||
const SECTION_HEADER_HEIGHT = 28;
|
||||
|
||||
export default class EmojiPicker extends PureComponent {
|
||||
class EmojiPicker extends PureComponent {
|
||||
static propTypes = {
|
||||
fuse: PropTypes.object.isRequired,
|
||||
emojis: PropTypes.array.isRequired,
|
||||
emojisBySection: PropTypes.array.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
onEmojiPress: PropTypes.func,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
@@ -46,116 +33,46 @@ export default class EmojiPicker extends PureComponent {
|
||||
onEmojiPress: emptyFunction
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired
|
||||
leftButton = {
|
||||
id: 'close-edit-post'
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.sectionListGetItemLayout = sectionListGetItemLayout({
|
||||
getItemHeight: () => {
|
||||
return EMOJI_SIZE + (EMOJI_GUTTER * 2);
|
||||
},
|
||||
getSectionHeaderHeight: () => SECTION_HEADER_HEIGHT
|
||||
});
|
||||
|
||||
const emojis = this.renderableEmojis(props.emojisBySection, props.deviceWidth);
|
||||
const emojiSectionIndexByOffset = this.measureEmojiSections(emojis);
|
||||
|
||||
this.isX = DeviceInfo.getModel() === 'iPhone X';
|
||||
this.scrollToSectionTries = 0;
|
||||
this.state = {
|
||||
emojis,
|
||||
emojiSectionIndexByOffset,
|
||||
filteredEmojis: [],
|
||||
emojis: props.emojis,
|
||||
searchTerm: '',
|
||||
currentSectionIndex: 0
|
||||
width: props.deviceWidth - (SECTION_MARGIN * 2)
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.deviceWidth !== nextProps.deviceWidth) {
|
||||
if (nextProps.deviceWidth !== this.props.deviceWidth) {
|
||||
this.setState({
|
||||
emojis: this.renderableEmojis(this.props.emojisBySection, nextProps.deviceWidth)
|
||||
width: nextProps.deviceWidth - (SECTION_MARGIN * 2)
|
||||
});
|
||||
|
||||
if (this.refs.search_bar) {
|
||||
this.refs.search_bar.blur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderableEmojis = (emojis, deviceWidth) => {
|
||||
const numberOfColumns = this.getNumberOfColumns(deviceWidth);
|
||||
|
||||
const nextEmojis = emojis.map((section) => {
|
||||
const data = [];
|
||||
let row = {
|
||||
key: `${section.key}-0`,
|
||||
items: []
|
||||
};
|
||||
|
||||
section.data.forEach((emoji, index) => {
|
||||
if (index % numberOfColumns === 0 && index !== 0) {
|
||||
data.push(row);
|
||||
row = {
|
||||
key: `${section.key}-${index}`,
|
||||
items: []
|
||||
};
|
||||
}
|
||||
|
||||
row.items.push(emoji);
|
||||
});
|
||||
|
||||
if (row.items.length) {
|
||||
if (row.items.length < numberOfColumns) {
|
||||
// push some empty items to make sure flexbox can justfiy content correctly
|
||||
const emptyEmojis = new Array(numberOfColumns - row.items.length);
|
||||
row.items.push(...emptyEmojis);
|
||||
}
|
||||
|
||||
data.push(row);
|
||||
}
|
||||
|
||||
return {
|
||||
...section,
|
||||
data
|
||||
};
|
||||
});
|
||||
|
||||
return nextEmojis;
|
||||
};
|
||||
|
||||
measureEmojiSections = (emojiSections) => {
|
||||
let lastOffset = 0;
|
||||
return emojiSections.map((section) => {
|
||||
const start = lastOffset;
|
||||
const nextOffset = (section.data.length * (EMOJI_SIZE + (EMOJI_GUTTER * 2))) + SECTION_HEADER_HEIGHT;
|
||||
lastOffset += nextOffset;
|
||||
|
||||
return start;
|
||||
});
|
||||
};
|
||||
|
||||
changeSearchTerm = (text) => {
|
||||
this.setState({
|
||||
searchTerm: text
|
||||
});
|
||||
|
||||
clearTimeout(this.searchTermTimeout);
|
||||
const timeout = text ? 100 : 0;
|
||||
const timeout = text ? 350 : 0;
|
||||
this.searchTermTimeout = setTimeout(() => {
|
||||
const filteredEmojis = this.searchEmojis(text);
|
||||
const emojis = this.searchEmojis(text);
|
||||
this.setState({
|
||||
filteredEmojis
|
||||
emojis
|
||||
});
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
cancelSearch = () => {
|
||||
this.setState({
|
||||
filteredEmojis: [],
|
||||
emojis: this.props.emojis,
|
||||
searchTerm: ''
|
||||
});
|
||||
};
|
||||
@@ -165,117 +82,46 @@ export default class EmojiPicker extends PureComponent {
|
||||
};
|
||||
|
||||
searchEmojis = (searchTerm) => {
|
||||
const {emojis, fuse} = this.props;
|
||||
const {emojis} = this.props;
|
||||
const searchTermLowerCase = searchTerm.toLowerCase();
|
||||
|
||||
if (!searchTerm) {
|
||||
return [];
|
||||
return emojis;
|
||||
}
|
||||
|
||||
const results = fuse.search(searchTermLowerCase);
|
||||
const data = results.map((index) => emojis[index]);
|
||||
return data;
|
||||
};
|
||||
const nextEmojis = [];
|
||||
emojis.forEach((section) => {
|
||||
const {data, ...otherProps} = section;
|
||||
const {key, items} = data[0];
|
||||
|
||||
getNumberOfColumns = (deviceWidth) => {
|
||||
return Math.floor(Number(((deviceWidth - (SECTION_MARGIN * 2)) / (EMOJI_SIZE + (EMOJI_GUTTER * 2)))));
|
||||
};
|
||||
const nextData = {
|
||||
key,
|
||||
items: items.filter((item) => {
|
||||
if (item.aliases) {
|
||||
return this.filterEmojiAliases(item.aliases, searchTermLowerCase);
|
||||
}
|
||||
|
||||
renderItem = ({item}) => {
|
||||
return (
|
||||
<EmojiPickerRow
|
||||
key={item.key}
|
||||
emojiGutter={EMOJI_GUTTER}
|
||||
emojiSize={EMOJI_SIZE}
|
||||
items={item.items}
|
||||
onEmojiPress={this.props.onEmojiPress}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return item.name.includes(searchTermLowerCase);
|
||||
})
|
||||
};
|
||||
|
||||
flatListKeyExtractor = (item) => item;
|
||||
|
||||
flatListRenderItem = ({item}) => {
|
||||
const style = getStyleSheetFromTheme(this.props.theme);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => this.props.onEmojiPress(item)}
|
||||
style={style.flatListRow}
|
||||
>
|
||||
<View style={style.flatListEmoji}>
|
||||
<Emoji
|
||||
emojiName={item}
|
||||
size={20}
|
||||
/>
|
||||
</View>
|
||||
<Text style={style.flatListEmojiName}>{`:${item}:`}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
onScroll = (e) => {
|
||||
if (this.state.jumpToSection) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(this.setIndexTimeout);
|
||||
|
||||
const {contentOffset} = e.nativeEvent;
|
||||
let nextIndex = this.state.emojiSectionIndexByOffset.findIndex((offset) => contentOffset.y <= offset);
|
||||
|
||||
if (nextIndex === -1) {
|
||||
nextIndex = this.state.emojiSectionIndexByOffset.length - 1;
|
||||
} else if (nextIndex !== 0) {
|
||||
nextIndex -= 1;
|
||||
}
|
||||
|
||||
if (nextIndex !== this.state.currentSectionIndex) {
|
||||
this.setState({
|
||||
currentSectionIndex: nextIndex
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMomentumScrollEnd = () => {
|
||||
if (this.state.jumpToSection) {
|
||||
this.setState({
|
||||
jumpToSection: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
scrollToSection = (index) => {
|
||||
this.setState({
|
||||
jumpToSection: true,
|
||||
currentSectionIndex: index
|
||||
}, () => {
|
||||
this.sectionList.scrollToLocation({
|
||||
sectionIndex: index,
|
||||
itemIndex: 0,
|
||||
viewOffset: 25
|
||||
});
|
||||
if (nextData.items.length) {
|
||||
nextEmojis.push({
|
||||
...otherProps,
|
||||
data: [nextData]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleScrollToSectionFailed = ({index}) => {
|
||||
if (this.scrollToSectionTries < 1) {
|
||||
setTimeout(() => {
|
||||
this.scrollToSectionTries++;
|
||||
this.scrollToSection(index);
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
return nextEmojis;
|
||||
};
|
||||
|
||||
renderSectionHeader = ({section}) => {
|
||||
const {theme} = this.props;
|
||||
const styles = getStyleSheetFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.sectionTitleContainer}
|
||||
key={section.title}
|
||||
>
|
||||
<View key={section.title}>
|
||||
<FormattedText
|
||||
style={styles.sectionTitle}
|
||||
id={section.id}
|
||||
@@ -285,148 +131,111 @@ export default class EmojiPicker extends PureComponent {
|
||||
);
|
||||
};
|
||||
|
||||
handleSectionIconPress = (index) => {
|
||||
this.scrollToSectionTries = 0;
|
||||
this.scrollToSection(index);
|
||||
}
|
||||
|
||||
renderSectionIcons = () => {
|
||||
renderEmojis = (emojis, index) => {
|
||||
const {theme} = this.props;
|
||||
const styles = getStyleSheetFromTheme(theme);
|
||||
|
||||
return this.state.emojis.map((section, index) => {
|
||||
const onPress = () => this.handleSectionIconPress(index);
|
||||
return (
|
||||
<View
|
||||
key={index}
|
||||
style={styles.columnStyle}
|
||||
>
|
||||
{emojis.map((emoji, emojiIndex) => {
|
||||
const style = [styles.emoji];
|
||||
if (emojiIndex === 0) {
|
||||
style.push(styles.emojiLeft);
|
||||
} else if (emojiIndex === emojis.length - 1) {
|
||||
style.push(styles.emojiRight);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={section.key}
|
||||
onPress={onPress}
|
||||
style={styles.sectionIconContainer}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
name={section.icon}
|
||||
size={17}
|
||||
style={[styles.sectionIcon, (index === this.state.currentSectionIndex && styles.sectionIconHighlight)]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={emoji.name}
|
||||
style={style}
|
||||
onPress={() => {
|
||||
this.props.onEmojiPress(emoji.name);
|
||||
}}
|
||||
>
|
||||
<Emoji
|
||||
emojiName={emoji.name}
|
||||
size={EMOJI_SIZE}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
attachSectionList = (c) => {
|
||||
this.sectionList = c;
|
||||
renderItem = ({item}) => {
|
||||
const {theme} = this.props;
|
||||
const styles = getStyleSheetFromTheme(theme);
|
||||
|
||||
const numColumns = Number((this.state.width / (EMOJI_SIZE + (EMOJI_GUTTER * 2))).toFixed(0));
|
||||
|
||||
const slices = item.items.reduce((slice, emoji, emojiIndex) => {
|
||||
if (emojiIndex % numColumns === 0 && emojiIndex !== 0) {
|
||||
slice.push([]);
|
||||
}
|
||||
|
||||
slice[slice.length - 1].push(emoji);
|
||||
|
||||
return slice;
|
||||
}, [[]]);
|
||||
|
||||
return (
|
||||
<View style={styles.section}>
|
||||
{slices.map(this.renderEmojis)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {deviceWidth, isLandscape, theme} = this.props;
|
||||
const {emojis, filteredEmojis, searchTerm} = this.state;
|
||||
const {intl} = this.context;
|
||||
const {intl, theme} = this.props;
|
||||
const {emojis, searchTerm} = this.state;
|
||||
const {formatMessage} = intl;
|
||||
const styles = getStyleSheetFromTheme(theme);
|
||||
|
||||
let listComponent;
|
||||
if (searchTerm) {
|
||||
listComponent = (
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps='always'
|
||||
style={styles.flatList}
|
||||
data={filteredEmojis}
|
||||
keyExtractor={this.flatListKeyExtractor}
|
||||
renderItem={this.flatListRenderItem}
|
||||
pageSize={10}
|
||||
initialListSize={10}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
listComponent = (
|
||||
<SectionList
|
||||
ref={this.attachSectionList}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={[styles.listView, {width: deviceWidth - (SECTION_MARGIN * 2)}]}
|
||||
sections={emojis}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
renderItem={this.renderItem}
|
||||
keyboardShouldPersistTaps='always'
|
||||
getItemLayout={this.sectionListGetItemLayout}
|
||||
removeClippedSubviews={true}
|
||||
onScroll={this.onScroll}
|
||||
onScrollToIndexFailed={this.handleScrollToSectionFailed}
|
||||
onMomentumScrollEnd={this.onMomentumScrollEnd}
|
||||
pageSize={30}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let keyboardOffset = 64;
|
||||
if (Platform.OS === 'android') {
|
||||
keyboardOffset = -200;
|
||||
} else if (this.isX) {
|
||||
keyboardOffset = isLandscape ? 35 : 107;
|
||||
} else if (isLandscape) {
|
||||
keyboardOffset = 52;
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView excludeHeader={true}>
|
||||
<KeyboardAvoidingView
|
||||
behavior='padding'
|
||||
style={{flex: 1}}
|
||||
keyboardVerticalOffset={keyboardOffset}
|
||||
>
|
||||
<View style={styles.searchBar}>
|
||||
<SearchBar
|
||||
ref='search_bar'
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
backgroundColor='transparent'
|
||||
inputHeight={33}
|
||||
inputStyle={{
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 13
|
||||
}}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
|
||||
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
titleCancelColor={theme.centerChannelColor}
|
||||
onChangeText={this.changeSearchTerm}
|
||||
onCancelButtonPress={this.cancelSearch}
|
||||
value={searchTerm}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.container}>
|
||||
{listComponent}
|
||||
{!searchTerm &&
|
||||
<View style={styles.bottomContentWrapper}>
|
||||
<View style={styles.bottomContent}>
|
||||
{this.renderSectionIcons()}
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
<View style={styles.wrapper}>
|
||||
<View style={styles.searchBar}>
|
||||
<SearchBar
|
||||
ref='search_bar'
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
backgroundColor='transparent'
|
||||
inputHeight={33}
|
||||
inputStyle={{
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 13
|
||||
}}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
|
||||
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
titleCancelColor={theme.centerChannelColor}
|
||||
onChangeText={this.changeSearchTerm}
|
||||
onCancelButtonPress={this.cancelSearch}
|
||||
value={searchTerm}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.container}>
|
||||
<SectionList
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={styles.listView}
|
||||
sections={emojis}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
renderItem={this.renderItem}
|
||||
removeClippedSubviews={true}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
bottomContent: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.3),
|
||||
borderTopWidth: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
bottomContentWrapper: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 35,
|
||||
width: '100%'
|
||||
},
|
||||
columnStyle: {
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'row',
|
||||
@@ -451,34 +260,8 @@ const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
emojiRight: {
|
||||
marginRight: 0
|
||||
},
|
||||
flatList: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
alignSelf: 'stretch'
|
||||
},
|
||||
flatListEmoji: {
|
||||
marginRight: 5
|
||||
},
|
||||
flatListEmojiName: {
|
||||
fontSize: 13,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
flatListRow: {
|
||||
height: 40,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 8,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
},
|
||||
listView: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
marginBottom: 35
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
searchBar: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
@@ -487,30 +270,16 @@ const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
section: {
|
||||
alignItems: 'center'
|
||||
},
|
||||
sectionIcon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.3)
|
||||
},
|
||||
sectionIconContainer: {
|
||||
flex: 1,
|
||||
height: 35,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
sectionIconHighlight: {
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
sectionTitle: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
fontSize: 15,
|
||||
fontWeight: '700'
|
||||
},
|
||||
sectionTitleContainer: {
|
||||
height: SECTION_HEADER_HEIGHT,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: theme.centerChannelBg
|
||||
fontWeight: '700',
|
||||
paddingVertical: 5
|
||||
},
|
||||
wrapper: {
|
||||
flex: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export default injectIntl(EmojiPicker);
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import Emoji from 'app/components/emoji';
|
||||
|
||||
export default class EmojiPickerRow extends Component {
|
||||
static propTypes = {
|
||||
emojiGutter: PropTypes.number.isRequired,
|
||||
emojiSize: PropTypes.number.isRequired,
|
||||
items: PropTypes.array.isRequired,
|
||||
onEmojiPress: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return this.props.items.length !== nextProps.items.length;
|
||||
}
|
||||
|
||||
renderEmojis = (emoji, index, emojis) => {
|
||||
const {emojiGutter, emojiSize} = this.props;
|
||||
|
||||
const style = [
|
||||
styles.emoji,
|
||||
{
|
||||
width: emojiSize,
|
||||
height: emojiSize,
|
||||
marginHorizontal: emojiGutter
|
||||
}
|
||||
];
|
||||
if (index === 0) {
|
||||
style.push(styles.emojiLeft);
|
||||
} else if (index === emojis.length - 1) {
|
||||
style.push(styles.emojiRight);
|
||||
}
|
||||
|
||||
if (!emoji) {
|
||||
return (
|
||||
<View
|
||||
key={index}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={emoji.name}
|
||||
style={style}
|
||||
onPress={() => {
|
||||
this.props.onEmojiPress(emoji.name);
|
||||
}}
|
||||
>
|
||||
<Emoji
|
||||
emojiName={emoji.name}
|
||||
size={emojiSize}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {emojiGutter, items} = this.props;
|
||||
|
||||
return (
|
||||
<View style={[styles.columnStyle, {marginVertical: emojiGutter}]}>
|
||||
{items.map(this.renderEmojis)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
columnStyle: {
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
emoji: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
emojiLeft: {
|
||||
marginLeft: 0
|
||||
},
|
||||
emojiRight: {
|
||||
marginRight: 0
|
||||
}
|
||||
});
|
||||
@@ -6,63 +6,48 @@ import {createSelector} from 'reselect';
|
||||
|
||||
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
|
||||
|
||||
import {getDimensions, isLandscape} from 'app/selectors/device';
|
||||
import {getDimensions} from 'app/selectors/device';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {CategoryNames, Emojis, EmojiIndicesByAlias, EmojiIndicesByCategory} from 'app/utils/emojis';
|
||||
import {CategoryNames, Emojis, EmojiIndicesByCategory} from 'app/utils/emojis';
|
||||
|
||||
import EmojiPicker from './emoji_picker';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
const categoryToI18n = {
|
||||
activity: {
|
||||
id: 'mobile.emoji_picker.activity',
|
||||
defaultMessage: 'ACTIVITY',
|
||||
icon: 'futbol-o'
|
||||
defaultMessage: 'ACTIVITY'
|
||||
},
|
||||
custom: {
|
||||
id: 'mobile.emoji_picker.custom',
|
||||
defaultMessage: 'CUSTOM',
|
||||
icon: 'at'
|
||||
defaultMessage: 'CUSTOM'
|
||||
},
|
||||
flags: {
|
||||
id: 'mobile.emoji_picker.flags',
|
||||
defaultMessage: 'FLAGS',
|
||||
icon: 'flag-o'
|
||||
defaultMessage: 'FLAGS'
|
||||
},
|
||||
foods: {
|
||||
id: 'mobile.emoji_picker.foods',
|
||||
defaultMessage: 'FOODS',
|
||||
icon: 'cutlery'
|
||||
defaultMessage: 'FOODS'
|
||||
},
|
||||
nature: {
|
||||
id: 'mobile.emoji_picker.nature',
|
||||
defaultMessage: 'NATURE',
|
||||
icon: 'leaf'
|
||||
defaultMessage: 'NATURE'
|
||||
},
|
||||
objects: {
|
||||
id: 'mobile.emoji_picker.objects',
|
||||
defaultMessage: 'OBJECTS',
|
||||
icon: 'lightbulb-o'
|
||||
defaultMessage: 'OBJECTS'
|
||||
},
|
||||
people: {
|
||||
id: 'mobile.emoji_picker.people',
|
||||
defaultMessage: 'PEOPLE',
|
||||
icon: 'smile-o'
|
||||
defaultMessage: 'PEOPLE'
|
||||
},
|
||||
places: {
|
||||
id: 'mobile.emoji_picker.places',
|
||||
defaultMessage: 'PLACES',
|
||||
icon: 'plane'
|
||||
},
|
||||
recent: {
|
||||
id: 'mobile.emoji_picker.recent',
|
||||
defaultMessage: 'RECENTLY USED',
|
||||
icon: 'clock-o'
|
||||
defaultMessage: 'PLACES'
|
||||
},
|
||||
symbols: {
|
||||
id: 'mobile.emoji_picker.symbols',
|
||||
defaultMessage: 'SYMBOLS',
|
||||
icon: 'heart-o'
|
||||
defaultMessage: 'SYMBOLS'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -76,24 +61,28 @@ function fillEmoji(indice) {
|
||||
|
||||
const getEmojisBySection = createSelector(
|
||||
getCustomEmojisByName,
|
||||
(state) => state.views.recentEmojis,
|
||||
(customEmojis, recentEmojis) => {
|
||||
(customEmojis) => {
|
||||
const emoticons = CategoryNames.filter((name) => name !== 'custom').map((category) => {
|
||||
const items = EmojiIndicesByCategory.get(category).map(fillEmoji);
|
||||
|
||||
const section = {
|
||||
...categoryToI18n[category],
|
||||
key: category,
|
||||
data: items
|
||||
data: [{
|
||||
key: `${category}-emojis`,
|
||||
items: EmojiIndicesByCategory.get(category).map(fillEmoji)
|
||||
}]
|
||||
};
|
||||
|
||||
return section;
|
||||
});
|
||||
|
||||
const customEmojiItems = [];
|
||||
const customEmojiData = {
|
||||
key: 'custom-emojis',
|
||||
title: 'CUSTOM',
|
||||
items: []
|
||||
};
|
||||
|
||||
for (const [key] of customEmojis) {
|
||||
customEmojiItems.push({
|
||||
customEmojiData.items.push({
|
||||
name: key
|
||||
});
|
||||
}
|
||||
@@ -101,57 +90,20 @@ const getEmojisBySection = createSelector(
|
||||
emoticons.push({
|
||||
...categoryToI18n.custom,
|
||||
key: 'custom',
|
||||
data: customEmojiItems
|
||||
data: [customEmojiData]
|
||||
});
|
||||
|
||||
if (recentEmojis.length) {
|
||||
const items = recentEmojis.map((emoji) => ({name: emoji}));
|
||||
|
||||
emoticons.unshift({
|
||||
...categoryToI18n.recent,
|
||||
key: 'recent',
|
||||
data: items
|
||||
});
|
||||
}
|
||||
|
||||
return emoticons;
|
||||
}
|
||||
);
|
||||
|
||||
const getEmojisByName = createSelector(
|
||||
getCustomEmojisByName,
|
||||
(customEmojis) => {
|
||||
const emoticons = new Set();
|
||||
for (const [key] of [...EmojiIndicesByAlias.entries(), ...customEmojis.entries()]) {
|
||||
emoticons.add(key);
|
||||
}
|
||||
|
||||
return Array.from(emoticons);
|
||||
}
|
||||
);
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const emojisBySection = getEmojisBySection(state);
|
||||
const emojis = getEmojisByName(state);
|
||||
const emojis = getEmojisBySection(state);
|
||||
const {deviceWidth} = getDimensions(state);
|
||||
const options = {
|
||||
shouldSort: true,
|
||||
threshold: 0.3,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
minMatchCharLength: 2,
|
||||
maxPatternLength: 32
|
||||
};
|
||||
|
||||
const list = emojis.length ? emojis : [];
|
||||
const fuse = new Fuse(list, options);
|
||||
|
||||
return {
|
||||
fuse,
|
||||
emojis,
|
||||
emojisBySection,
|
||||
deviceWidth,
|
||||
isLandscape: isLandscape(state),
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {GlobalStyles} from 'app/styles';
|
||||
@@ -15,12 +14,11 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
class ErrorText extends PureComponent {
|
||||
static propTypes = {
|
||||
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
textStyle: CustomPropTypes.Style,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const {error, textStyle, theme} = this.props;
|
||||
const {error, theme} = this.props;
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
@@ -34,13 +32,13 @@ class ErrorText extends PureComponent {
|
||||
id={intl.id}
|
||||
defaultMessage={intl.defaultMessage}
|
||||
values={intl.values}
|
||||
style={[GlobalStyles.errorLabel, style.errorLabel, textStyle]}
|
||||
style={[GlobalStyles.errorLabel, style.errorLabel]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text style={[GlobalStyles.errorLabel, style.errorLabel, textStyle]}>
|
||||
<Text style={[GlobalStyles.errorLabel, style.errorLabel]}>
|
||||
{error.message || error}
|
||||
</Text>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import * as Utils from 'mattermost-redux/utils/file_utils.js';
|
||||
|
||||
import FileAttachmentDocument, {SUPPORTED_DOCS_FORMAT} from './file_attachment_document';
|
||||
import FileAttachmentIcon from './file_attachment_icon';
|
||||
import FileAttachmentImage from './file_attachment_image';
|
||||
|
||||
@@ -29,11 +28,7 @@ export default class FileAttachment extends PureComponent {
|
||||
static defaultProps = {
|
||||
onInfoPress: () => true,
|
||||
onPreviewPress: () => true
|
||||
};
|
||||
|
||||
handlePreviewPress = () => {
|
||||
this.props.onPreviewPress(this.props.file);
|
||||
};
|
||||
}
|
||||
|
||||
renderFileInfo() {
|
||||
const {file, theme} = this.props;
|
||||
@@ -60,50 +55,40 @@ export default class FileAttachment extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {file, onInfoPress, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
handlePreviewPress = () => {
|
||||
this.props.onPreviewPress(this.props.file);
|
||||
}
|
||||
|
||||
let mime = file.mime_type;
|
||||
if (mime && mime.includes(';')) {
|
||||
mime = mime.split(';')[0];
|
||||
}
|
||||
render() {
|
||||
const {file, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let fileAttachmentComponent;
|
||||
if (file.has_preview_image || file.loading || file.mime_type === 'image/gif') {
|
||||
fileAttachmentComponent = (
|
||||
<TouchableOpacity onPress={this.handlePreviewPress}>
|
||||
<FileAttachmentImage
|
||||
addFileToFetchCache={this.props.addFileToFetchCache}
|
||||
fetchCache={this.props.fetchCache}
|
||||
file={file}
|
||||
theme={theme}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
} else if (SUPPORTED_DOCS_FORMAT.includes(mime)) {
|
||||
fileAttachmentComponent = (
|
||||
<FileAttachmentDocument
|
||||
<FileAttachmentImage
|
||||
addFileToFetchCache={this.props.addFileToFetchCache}
|
||||
fetchCache={this.props.fetchCache}
|
||||
file={file}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
fileAttachmentComponent = (
|
||||
<TouchableOpacity onPress={this.handlePreviewPress}>
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={theme}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.fileWrapper}>
|
||||
{fileAttachmentComponent}
|
||||
<TouchableOpacity onPress={this.handlePreviewPress}>
|
||||
{fileAttachmentComponent}
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={onInfoPress}
|
||||
onPress={this.props.onInfoPress}
|
||||
style={style.fileInfoContainer}
|
||||
>
|
||||
{this.renderFileInfo()}
|
||||
@@ -148,21 +133,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
marginTop: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2)
|
||||
},
|
||||
circularProgress: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
circularProgressContent: {
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
top: 0,
|
||||
left: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import OpenFile from 'react-native-doc-viewer';
|
||||
import RNFetchBlob from 'react-native-fetch-blob';
|
||||
import {AnimatedCircularProgress} from 'react-native-circular-progress';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import {getFileUrl} from 'mattermost-redux/utils/file_utils.js';
|
||||
|
||||
import {DeviceTypes} from 'app/constants/';
|
||||
|
||||
import FileAttachmentIcon from './file_attachment_icon';
|
||||
|
||||
const {DOCUMENTS_PATH} = DeviceTypes;
|
||||
export const SUPPORTED_DOCS_FORMAT = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/rtf',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/xml',
|
||||
'text/csv'
|
||||
];
|
||||
|
||||
export default class FileAttachmentDocument extends PureComponent {
|
||||
static propTypes = {
|
||||
iconHeight: PropTypes.number,
|
||||
iconWidth: PropTypes.number,
|
||||
file: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
wrapperHeight: PropTypes.number,
|
||||
wrapperWidth: PropTypes.number
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
iconHeight: 65,
|
||||
iconWidth: 65,
|
||||
wrapperHeight: 100,
|
||||
wrapperWidth: 100
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape
|
||||
};
|
||||
|
||||
state = {
|
||||
didCancel: false,
|
||||
downloading: false,
|
||||
progress: 0
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
cancelDownload = () => {
|
||||
if (this.mounted) {
|
||||
this.setState({didCancel: true});
|
||||
}
|
||||
|
||||
if (this.downloadTask) {
|
||||
this.downloadTask.cancel();
|
||||
}
|
||||
};
|
||||
|
||||
downloadAndPreviewFile = async (file) => {
|
||||
const path = `${DOCUMENTS_PATH}/${file.name}`;
|
||||
|
||||
this.setState({didCancel: false});
|
||||
|
||||
try {
|
||||
const isDir = await RNFetchBlob.fs.isDir(DOCUMENTS_PATH);
|
||||
if (!isDir) {
|
||||
try {
|
||||
await RNFetchBlob.fs.mkdir(DOCUMENTS_PATH);
|
||||
} catch (error) {
|
||||
this.showDownloadFailedAlert();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
session: file.id,
|
||||
timeout: 10000,
|
||||
indicator: true,
|
||||
overwrite: true,
|
||||
path
|
||||
};
|
||||
|
||||
const exist = await RNFetchBlob.fs.exists(path);
|
||||
if (exist) {
|
||||
this.openDocument(file, 0);
|
||||
} else {
|
||||
this.setState({downloading: true});
|
||||
this.downloadTask = RNFetchBlob.config(options).fetch('GET', getFileUrl(file.id));
|
||||
this.downloadTask.progress((received, total) => {
|
||||
const progress = (received / total) * 100;
|
||||
if (this.mounted) {
|
||||
this.setState({progress});
|
||||
}
|
||||
});
|
||||
|
||||
await this.downloadTask;
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
progress: 100
|
||||
}, () => {
|
||||
// need to wait a bit for the progress circle UI to update to the give progress
|
||||
this.openDocument(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
RNFetchBlob.fs.unlink(path);
|
||||
if (this.mounted) {
|
||||
this.setState({downloading: false, progress: 0});
|
||||
|
||||
if (error.message !== 'cancelled') {
|
||||
this.showDownloadFailedAlert();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handlePreviewPress = async () => {
|
||||
const {file} = this.props;
|
||||
const {downloading, progress} = this.state;
|
||||
|
||||
if (downloading && progress < 100) {
|
||||
this.cancelDownload();
|
||||
} else if (downloading) {
|
||||
this.resetViewState();
|
||||
} else {
|
||||
this.downloadAndPreviewFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
openDocument = (file, delay = 2000) => {
|
||||
// The animation for the progress circle takes about 2 seconds to finish
|
||||
// therefore we are delaying the opening of the document to have the UI
|
||||
// shown nicely and smooth
|
||||
setTimeout(() => {
|
||||
if (!this.state.didCancel && this.mounted) {
|
||||
const prefix = Platform.OS === 'android' ? 'file:/' : '';
|
||||
const path = `${DOCUMENTS_PATH}/${file.name}`;
|
||||
OpenFile.openDoc([{
|
||||
url: `${prefix}${path}`,
|
||||
fileName: file.name,
|
||||
fileType: file.extension,
|
||||
cache: false
|
||||
}], (error) => {
|
||||
if (error) {
|
||||
const {intl} = this.context;
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.document_preview.failed_title',
|
||||
defaultMessage: 'Open Document failed'
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.document_preview.failed_description',
|
||||
defaultMessage: 'An error occurred while opening the document. Please make sure you have a {fileType} viewer installed and try again.\n'
|
||||
}, {
|
||||
fileType: file.extension.toUpperCase()
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.server_upgrade.button',
|
||||
defaultMessage: 'OK'
|
||||
})
|
||||
}]
|
||||
);
|
||||
RNFetchBlob.fs.unlink(path);
|
||||
}
|
||||
this.setState({downloading: false, progress: 0});
|
||||
});
|
||||
}
|
||||
}, delay);
|
||||
};
|
||||
|
||||
resetViewState = () => {
|
||||
if (this.mounted) {
|
||||
this.setState({
|
||||
progress: 0,
|
||||
didCancel: true
|
||||
}, () => {
|
||||
// need to wait a bit for the progress circle UI to update to the give progress
|
||||
setTimeout(() => this.setState({downloading: false}), 2000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
renderProgress = () => {
|
||||
const {iconHeight, iconWidth, file, theme, wrapperWidth} = this.props;
|
||||
|
||||
return (
|
||||
<View style={[style.circularProgressContent, {width: wrapperWidth}]}>
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
iconHeight={iconHeight}
|
||||
iconWidth={iconWidth}
|
||||
theme={theme}
|
||||
wrapperHeight={iconHeight}
|
||||
wrapperWidth={iconWidth}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
showDownloadFailedAlert = () => {
|
||||
const {intl} = this.context;
|
||||
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.downloader.failed_title',
|
||||
defaultMessage: 'Download failed'
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.downloader.failed_description',
|
||||
defaultMessage: 'An error occurred while downloading the file. Please check your internet connection and try again.\n'
|
||||
}),
|
||||
[{
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.server_upgrade.button',
|
||||
defaultMessage: 'OK'
|
||||
})
|
||||
}]
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {iconHeight, iconWidth, file, theme, wrapperHeight, wrapperWidth} = this.props;
|
||||
const {downloading, progress} = this.state;
|
||||
|
||||
let fileAttachmentComponent;
|
||||
if (downloading) {
|
||||
fileAttachmentComponent = (
|
||||
<AnimatedCircularProgress
|
||||
size={wrapperHeight}
|
||||
fill={progress}
|
||||
width={4}
|
||||
backgroundColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColor={theme.linkColor}
|
||||
rotation={0}
|
||||
style={style.circularProgress}
|
||||
>
|
||||
{this.renderProgress}
|
||||
</AnimatedCircularProgress>
|
||||
);
|
||||
} else {
|
||||
fileAttachmentComponent = (
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={theme}
|
||||
iconHeight={iconHeight}
|
||||
iconWidth={iconWidth}
|
||||
wrapperHeight={wrapperHeight}
|
||||
wrapperWidth={wrapperWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={this.handlePreviewPress}>
|
||||
{fileAttachmentComponent}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
circularProgress: {
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
width: '100%'
|
||||
},
|
||||
circularProgressContent: {
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
top: 0
|
||||
}
|
||||
});
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
View,
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
@@ -58,7 +57,7 @@ export default class FileAttachmentList extends Component {
|
||||
navBarHidden: true,
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false,
|
||||
screenBackgroundColor: 'black',
|
||||
screenBackgroundColor: 'transparent',
|
||||
modalPresentationStyle: 'overCurrentContext'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ function makeMapStateToProps() {
|
||||
const getFilesForPost = makeGetFilesForPost();
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
fetchCache: state.views.fetchCache,
|
||||
files: getFilesForPost(state, ownProps.postId),
|
||||
theme: getTheme(state),
|
||||
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import FileAttachmentImage from 'app/components/file_attachment_list/file_attachment_image';
|
||||
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
|
||||
import KeyboardLayout from 'app/components/layout/keyboard_layout';
|
||||
|
||||
export default class FileUploadPreview extends PureComponent {
|
||||
static propTypes = {
|
||||
@@ -32,8 +32,7 @@ export default class FileUploadPreview extends PureComponent {
|
||||
inputHeight: PropTypes.number.isRequired,
|
||||
rootId: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
filesUploadingForCurrentChannel: PropTypes.bool.isRequired,
|
||||
showFileMaxWarning: PropTypes.bool.isRequired
|
||||
filesUploadingForCurrentChannel: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
handleRetryFileUpload = (file) => {
|
||||
@@ -47,7 +46,7 @@ export default class FileUploadPreview extends PureComponent {
|
||||
buildFilePreviews = () => {
|
||||
return this.props.files.map((file) => {
|
||||
let filePreviewComponent;
|
||||
if (file.loading | (file.has_preview_image || file.mime_type === 'image/gif')) {
|
||||
if (file.loading | file.has_preview_image) {
|
||||
filePreviewComponent = (
|
||||
<FileAttachmentImage
|
||||
addFileToFetchCache={this.props.actions.addFileToFetchCache}
|
||||
@@ -100,20 +99,13 @@ export default class FileUploadPreview extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
showFileMaxWarning,
|
||||
channelIsLoading,
|
||||
filesUploadingForCurrentChannel,
|
||||
deviceHeight,
|
||||
files
|
||||
} = this.props;
|
||||
if (channelIsLoading || (!files.length && !filesUploadingForCurrentChannel)) {
|
||||
if (this.props.channelIsLoading || (!this.props.files.length && !this.props.filesUploadingForCurrentChannel)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={[style.container, {height: deviceHeight}]}>
|
||||
<KeyboardLayout>
|
||||
<View style={[style.container, {height: this.props.deviceHeight}]}>
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
style={style.scrollView}
|
||||
@@ -121,16 +113,8 @@ export default class FileUploadPreview extends PureComponent {
|
||||
>
|
||||
{this.buildFilePreviews()}
|
||||
</ScrollView>
|
||||
{showFileMaxWarning && (
|
||||
<FormattedText
|
||||
style={style.warning}
|
||||
id='mobile.file_upload.max_warning'
|
||||
defaultMessage='Uploads limited to 5 files maximum.'
|
||||
/>
|
||||
)}
|
||||
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -203,10 +187,5 @@ const style = StyleSheet.create({
|
||||
scrollViewContent: {
|
||||
alignItems: 'flex-end',
|
||||
marginLeft: 14
|
||||
},
|
||||
warning: {
|
||||
color: 'white',
|
||||
marginLeft: 14,
|
||||
marginBottom: 12
|
||||
}
|
||||
});
|
||||
|
||||
@@ -33,6 +33,7 @@ function mapStateToProps(state, ownProps) {
|
||||
const {deviceHeight} = getDimensions(state);
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
channelIsLoading: state.views.channel.loading,
|
||||
createPostRequestStatus: state.requests.posts.createPost.status,
|
||||
deviceHeight,
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Svg, {Path, G} from 'react-native-svg';
|
||||
|
||||
export default class FlagIcon extends PureComponent {
|
||||
static propTypes = {
|
||||
width: PropTypes.number.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
color: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Svg
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
viewBox='0 0 16 16'
|
||||
>
|
||||
<G fill={this.props.color}>
|
||||
<Path
|
||||
d='M8,1 L2,1 C2,0.447 1.553,0 1,0 C0.447,0 0,0.447 0,1 L0,15.5 C0,15.776 0.224,16 0.5,16 L1.5,16 C1.776,16 2,15.776 2,15.5 L2,11 L7,11 L7,12 C7,12.553 7.447,13 8,13 L15,13 C15.553,13 16,12.553 16,12 L16,4 C16,3.447 15.553,3 15,3 L9,3 L9,2 C9,1.447 8.553,1 8,1 Z'
|
||||
fill={this.props.color}
|
||||
/>
|
||||
</G>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
157
app/components/inverted_flat_list/index.js
Normal file
157
app/components/inverted_flat_list/index.js
Normal file
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {FlatList, Platform, ScrollView, StyleSheet, View} from 'react-native';
|
||||
|
||||
import RefreshList from 'app/components/refresh_list';
|
||||
|
||||
import VirtualList from './virtual_list';
|
||||
|
||||
export default class InvertibleFlatList extends PureComponent {
|
||||
static propTypes = {
|
||||
horizontal: PropTypes.bool,
|
||||
inverted: PropTypes.bool,
|
||||
ListFooterComponent: PropTypes.func,
|
||||
renderItem: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
horizontal: false,
|
||||
inverted: true
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.inversionDirection = props.horizontal ? styles.horizontal : styles.vertical;
|
||||
}
|
||||
|
||||
getMetrics = () => {
|
||||
return this.flatListRef.getMetrics();
|
||||
};
|
||||
|
||||
recordInteraction = () => {
|
||||
this.flatListRef.recordInteraction();
|
||||
};
|
||||
|
||||
renderFooter = () => {
|
||||
const {ListFooterComponent: footer} = this.props;
|
||||
if (!footer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, this.inversionDirection]}>
|
||||
{footer()}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderItem = (info) => {
|
||||
return (
|
||||
<View style={[styles.container, this.inversionDirection]}>
|
||||
{this.props.renderItem(info)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderScrollComponent = (props) => {
|
||||
const {theme} = this.props;
|
||||
|
||||
if (props.onRefresh) {
|
||||
return (
|
||||
<ScrollView
|
||||
{...props}
|
||||
refreshControl={
|
||||
<RefreshList
|
||||
refreshing={props.refreshing}
|
||||
onRefresh={props.onRefresh}
|
||||
tintColor={theme.centerChannelColor}
|
||||
colors={[theme.centerChannelColor]}
|
||||
style={this.inversionDirection}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ScrollView {...props}/>;
|
||||
};
|
||||
|
||||
scrollToEnd = (params) => {
|
||||
this.flatListRef.scrollToEnd(params);
|
||||
};
|
||||
|
||||
scrollToIndex = (params) => {
|
||||
this.flatListRef.scrollToIndex(params);
|
||||
};
|
||||
|
||||
scrollToItem = (params) => {
|
||||
this.flatListRef.scrollToItem(params);
|
||||
};
|
||||
|
||||
scrollToOffset = (params) => {
|
||||
this.flatListRef.scrollToOffset(params);
|
||||
};
|
||||
|
||||
setFlatListRef = (flatListRef) => {
|
||||
this.flatListRef = flatListRef;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {inverted, ...forwardedProps} = this.props;
|
||||
|
||||
// If not inverted, render as an ordinary FlatList
|
||||
if (!inverted) {
|
||||
return (
|
||||
<FlatList
|
||||
{...forwardedProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, this.inversionDirection]}>
|
||||
<VirtualList
|
||||
ref={this.setFlatListRef}
|
||||
{...forwardedProps}
|
||||
ListFooterComponent={this.renderFooter}
|
||||
renderItem={this.renderItem}
|
||||
renderScrollComponent={this.renderScrollComponent}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
vertical: Platform.select({
|
||||
android: {
|
||||
transform: [
|
||||
{perspective: 1},
|
||||
{scaleY: -1}
|
||||
]
|
||||
},
|
||||
ios: {
|
||||
transform: [{scaleY: -1}]
|
||||
}
|
||||
}),
|
||||
horizontal: Platform.select({
|
||||
android: {
|
||||
transform: [
|
||||
{perspective: 1},
|
||||
{scaleY: -1}
|
||||
]
|
||||
},
|
||||
ios: {
|
||||
transform: [{scaleX: -1}]
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
54
app/components/inverted_flat_list/virtual_list.js
Normal file
54
app/components/inverted_flat_list/virtual_list.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {VirtualizedList} from 'react-native';
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
export default class Virtualized extends VirtualizedList {
|
||||
_onScroll = (e) => {
|
||||
if (this.props.onScroll) {
|
||||
this.props.onScroll(e);
|
||||
}
|
||||
const timestamp = e.timeStamp;
|
||||
const visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement);
|
||||
const contentLength = this._selectLength(e.nativeEvent.contentSize);
|
||||
const offset = this._selectOffset(e.nativeEvent.contentOffset);
|
||||
const dt = Math.max(1, timestamp - this._scrollMetrics.timestamp);
|
||||
const dOffset = offset - this._scrollMetrics.offset;
|
||||
const velocity = dOffset / dt;
|
||||
this._scrollMetrics = {contentLength, dt, offset, timestamp, velocity, visibleLength};
|
||||
const {data, getItemCount, onEndReached, onEndReachedThreshold, windowSize} = this.props;
|
||||
this._updateViewableItems(data);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const distanceFromEnd = contentLength - visibleLength - offset;
|
||||
const itemCount = getItemCount(data);
|
||||
if (this.state.last === itemCount - 1 &&
|
||||
distanceFromEnd <= onEndReachedThreshold &&
|
||||
(this._hasDataChangedSinceEndReached ||
|
||||
this._scrollMetrics.contentLength !== this._sentEndForContentLength)) {
|
||||
// Only call onEndReached once for a given dataset + content length.
|
||||
this._hasDataChangedSinceEndReached = false;
|
||||
this._sentEndForContentLength = this._scrollMetrics.contentLength;
|
||||
onEndReached({distanceFromEnd});
|
||||
}
|
||||
const {first, last} = this.state;
|
||||
if ((first > 0 && velocity < 0) || (last < itemCount - 1 && velocity > 0)) {
|
||||
const distanceToContentEdge = Math.min(
|
||||
Math.abs(this._getFrameMetricsApprox(first).offset - offset),
|
||||
Math.abs(this._getFrameMetricsApprox(last).offset - (offset + visibleLength)),
|
||||
);
|
||||
const hiPri = distanceToContentEdge < (windowSize * visibleLength / 4);
|
||||
if (hiPri) {
|
||||
// Don't worry about interactions when scrolling quickly; focus on filling content as fast
|
||||
// as possible.
|
||||
this._updateCellsToRenderBatcher.dispose({abort: true});
|
||||
this._updateCellsToRender();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._updateCellsToRenderBatcher.schedule();
|
||||
};
|
||||
}
|
||||
@@ -3,16 +3,13 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {getStatusBarHeight} from 'app/selectors/device';
|
||||
|
||||
import KeyboardLayout from './keyboard_layout';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
statusBarHeight: getStatusBarHeight(state),
|
||||
theme: getTheme(state)
|
||||
statusBarHeight: getStatusBarHeight(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,98 +3,46 @@
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Animated, Keyboard, Platform, View} from 'react-native';
|
||||
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const {View: AnimatedView} = Animated;
|
||||
import {KeyboardAvoidingView, Platform, View} from 'react-native';
|
||||
|
||||
export default class KeyboardLayout extends PureComponent {
|
||||
static propTypes = {
|
||||
behaviour: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
statusBarHeight: PropTypes.number,
|
||||
theme: PropTypes.object.isRequired
|
||||
keyboardVerticalOffset: PropTypes.number,
|
||||
statusBarHeight: PropTypes.number
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
keyboardVerticalOffset: 0
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.subscriptions = [];
|
||||
this.count = 0;
|
||||
this.state = {
|
||||
bottom: new Animated.Value(0)
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (Platform.OS === 'ios') {
|
||||
this.subscriptions = [
|
||||
Keyboard.addListener('keyboardWillChangeFrame', this.onKeyboardChange),
|
||||
Keyboard.addListener('keyboardWillHide', this.onKeyboardWillHide)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscriptions.forEach((sub) => sub.remove());
|
||||
}
|
||||
|
||||
onKeyboardWillHide = (e) => {
|
||||
const {duration} = e;
|
||||
Animated.timing(this.state.bottom, {
|
||||
toValue: 0,
|
||||
duration
|
||||
}).start();
|
||||
};
|
||||
|
||||
onKeyboardChange = (e) => {
|
||||
if (!e) {
|
||||
this.setState({bottom: new Animated.Value(0)});
|
||||
return;
|
||||
}
|
||||
|
||||
const {endCoordinates, duration} = e;
|
||||
const {height} = endCoordinates;
|
||||
Animated.timing(this.state.bottom, {
|
||||
toValue: height,
|
||||
duration
|
||||
}).start();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {children, theme, ...otherProps} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
const {behaviour, children, keyboardVerticalOffset, statusBarHeight, ...otherProps} = this.props;
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
return (
|
||||
<View
|
||||
style={style.keyboardLayout}
|
||||
{...otherProps}
|
||||
>
|
||||
<View {...otherProps}>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let height = 0;
|
||||
if (statusBarHeight > 20) {
|
||||
height = (statusBarHeight - 20) + keyboardVerticalOffset;
|
||||
} else {
|
||||
height = keyboardVerticalOffset;
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatedView
|
||||
style={[style.keyboardLayout, {bottom: this.state.bottom}]}
|
||||
<KeyboardAvoidingView
|
||||
behaviour={behaviour}
|
||||
keyboardVerticalOffset={height}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</AnimatedView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
keyboardLayout: {
|
||||
position: 'relative',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
flex: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user