Compare commits

..

43 Commits

Author SHA1 Message Date
Elias Nahum
7f2a8d89eb Bump to build 64 2017-11-15 18:21:00 -03:00
Elias Nahum
ec44ca6f5e Add missed unit test fix 2017-11-15 18:18:46 -03:00
Elias Nahum
8d1ab7bcab Bump app version to 1.4.1 2017-11-15 18:18:46 -03:00
Chris Duarte
f011ecc3ec Use new isFavoriteChannel redux util (#1113)
* Use new isFavoriteChannel redux util

* Remove unmarkFavorite actions
2017-11-15 18:18:46 -03:00
Elias Nahum
240e52da6b Mark initial channel as read 2017-11-15 18:18:46 -03:00
Elias Nahum
36d2087660 Fix handleSubmit to use local state 2017-11-15 18:18:46 -03:00
Harrison Healey
c877d983cc Increased clickable area around send post button (#1139)
* Increased clickable area around send post button

* Fixed attachment icon not staying at the bottom when the post textbox gets taller
2017-11-15 18:18:46 -03:00
enahum
aa5f0c9557 Improve performance on post textbox (#1138) 2017-11-15 18:18:46 -03:00
enahum
20168d90ca Network detection fixed to use default if no server is present (#1137) 2017-11-15 18:18:46 -03:00
enahum
f15223abfe Network detection by pinging Mattermost server (#1130) 2017-11-15 18:18:46 -03:00
enahum
384898a0ac Fix badge render (#1134) 2017-11-15 18:18:46 -03:00
enahum
c42e2276a2 Prevent notify props from causing a crash (#1127) 2017-11-15 18:18:46 -03:00
lfbrock
2dcf7a01d0 Spelling correction (#1132)
* Spelling correction

* Update en.json

* fix eslint
2017-11-15 18:18:46 -03:00
enahum
979e899051 Fix iOS RN notification lib (#1128) 2017-11-15 18:18:46 -03:00
Harrison Healey
3f0a4ef153 RN-472 Improved handling of deleted search results (#1119) 2017-11-15 18:18:46 -03:00
Harrison Healey
4b57f4fed0 RN-487 Added null check for user in UserProfile (#1120) 2017-11-15 18:18:46 -03:00
Elias Nahum
ea3bcf66f4 Add validations in cleanup middleware (#1122) 2017-11-15 18:18:46 -03:00
Harrison Healey
610ed8e68d RN-471/RN-484 Fixed null pointer exceptions caused by ScrollViews (#1116)
* RN-471 Fixed null pointer from Swiper scroll view

* RN-484 Added null check to ImagePreview scroll view
2017-11-15 18:18:46 -03:00
Harrison Healey
3c19603fed RN-474 Fixed resetStateForNewVersion not working if upgrading from version without search state (#1117) 2017-11-15 18:18:46 -03:00
Harrison Healey
cb5970587f RN-414 Reduce loading of posts in increasePostVisibility action (#1111) 2017-11-15 18:18:46 -03:00
enahum
428e6225f5 Update fastlane (#1107) 2017-11-15 18:18:46 -03:00
Harrison Healey
585d7bc1e1 Changed PostProfilePicture to be a PureComponent (#1105)
* Changed PostProfilePicture to be a PureComponent

* Updating style
2017-11-15 18:18:46 -03:00
enahum
80906a776b translations PR 20171106 (#1103) 2017-11-15 18:18:46 -03:00
Elias Nahum
93265b3de0 Keep post openGraph data 2017-11-06 14:21:01 -03:00
enahum
0d70372a3c Version Bump to 63 (#1095) 2017-11-03 17:51:08 -03:00
enahum
72087391dc Version Bump to 63 (#1094) 2017-11-03 17:20:37 -03:00
enahum
9c89fe2907 Set the section list to wait for interactions before updating the unread indicator (#1093) 2017-11-03 14:47:31 -03:00
enahum
9684328123 set appSarted as false when logging out and resetting cache (#1092) 2017-11-03 14:47:18 -03:00
enahum
c3ef5e6f38 Version Bump to 62 (#1089) 2017-11-02 17:50:26 -03:00
enahum
e36f63c84d Version Bump to 62 (#1088) 2017-11-02 17:44:18 -03:00
enahum
ce05b9c98b RN-456 when a channel is left we update content and title (#1087) 2017-11-02 17:16:08 -03:00
enahum
388294a124 Update Mattermost redux (#1086)
* Fix middleware

* Upgrade mattermost-redux

* another middleware fix
2017-11-02 16:09:53 -03:00
enahum
1a12abfe50 Fix bugs reported by sentry (#1081) 2017-11-02 16:09:01 -03:00
Elias Nahum
0210d6e1eb Use ImageBackground for youtube videos instead of nested Images 2017-11-02 16:08:43 -03:00
enahum
49bcf185e6 Prevent More unreads indicators when canceling jump to... (#1082) 2017-11-01 18:50:28 -03:00
enahum
61ecf7d159 Version Bump to 61 (#1078) 2017-11-01 16:32:01 -03:00
enahum
ead5f2860f Version Bump to 61 (#1077) 2017-11-01 16:31:48 -03:00
enahum
09ac903630 Keep user statuses for current channel and all post visibilities (#1080) 2017-11-01 16:26:24 -03:00
enahum
a471379cb2 Perform validations to avoid crash on data cleanup (#1075)
* Perform validations to avoid crash on data cleanup

* Remove postId from postsInChannel
2017-10-31 12:23:27 -07:00
enahum
eaf128b2a0 Show profile images and names only for DMs in channel intro (#1076) 2017-10-31 12:23:17 -07:00
enahum
96f5cd2c11 translations PR 20171030 (#1074) 2017-10-31 10:42:31 -03:00
enahum
6b23c230ed Version Bump to 60 (#1073) 2017-10-30 14:37:35 -03:00
enahum
63a3e4eb89 Version Bump to 60 (#1072) 2017-10-30 14:37:18 -03:00
601 changed files with 18903 additions and 54970 deletions

View File

@@ -40,7 +40,7 @@
"brace-style": [2, "1tbs", { "allowSingleLine": false }],
"camelcase": [2, {"properties": "never"}],
"class-methods-use-this": 0,
"comma-dangle": [2, "always-multiline"],
"comma-dangle": [2, "never"],
"comma-spacing": [2, {"before": false, "after": true}],
"comma-style": [2, "last"],
"complexity": [1, 10],
@@ -54,7 +54,7 @@
"eqeqeq": [2, "smart"],
"func-call-spacing": [2, "never"],
"func-names": 2,
"func-style": [2, "declaration", { "allowArrowFunctions": true }],
"func-style": [2, "declaration"],
"generator-star-spacing": [0, {"before": false, "after": true}],
"global-require": 2,
"guard-for-in": 2,
@@ -259,4 +259,4 @@
"yoda": [2, "never", {"exceptRange": false, "onlyEquality": false}],
"mocha/no-exclusive-tests": 2
}
}
}

View File

@@ -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

3
.gitignore vendored
View File

@@ -82,6 +82,3 @@ ios/sentry.properties
# Pods
.podinstall
ios/Pods/
#editor-settings
.vscode

View File

@@ -1,172 +1,5 @@
# Mattermost Mobile Apps Changelog
## v1.7.1 Release
- Release Date: April 3, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue where the iOS share extension sometimes crashed the Mattermost app
- Fixed an issue preventing Markdown tables from rendering with some international characters
## v1.7.0 Release
- Release Date: March 26, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### iOS File Sharing
- Share files and images from other applications as attached files in Mattermost
#### Markdown Tables
- Tables created using markdown formatting can now be viewed in the app
#### Permalinks
- Permalinks now open in the app instead of launching a browser window
### Improvements
- Increased the tappable area of various icons for improved usability
- Announcement banners now display in the app
- Added "+" button to add emoji reactions to a post
- Minor performance improvements for app launch time
- Text files can now be viewed in the app
- Support for email autolinking into the app
### Bugs
- Fixed an issue causing some devices to hang at the splash screen on app launch
- Fixed an issue causing some letters to be hidden in the Android search input box
- Fixed an issue causing some Direct Message channels to show date stamps below the most recent message
- Fixed an issue where users weren't able to join open teams they've never been a member of
- Fixed an issue so double tapping buttons can no longer cause UI issues
- Fixed an issue where changing the channel display name wasn't being updated in the UI appropriately
- Fixed an issue where searhing for public channels sometimes showed no results
- Fixed an issue where the post menu could remain open while scrolling in the post list
- Fixed an issue where the system message to add users to a channel was missing the execution link
- Fixed an issue where bulleted lists cut off text if nested deeper than two levels
- Fixed an issue where logging into an account that is not on any team freezes the app
- Fixed an issue on iOS causing the app to crash when taking a photo then attaching it to a post
## v1.6.1 Release
- Release Date: February 13, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue preventing the app from going to the correct channel when opened from a push notification
- Fixed an issue on Android devices where the app could sometimes freeze on the launch screen
- Fixed an issue on Samsung devices causing extra letters to be insterted when typing to filter user lists
## v1.6.0 Release
- Release Date: February 6, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Android File Sharing
- Share files and images from other applications as attached files in Mattermost
### Improvements
- Added a right drawer to access settings, edit profile information, change online status and logout
- Added support for opening a Direct Message channel with yourself
### Bugs
- Fixed a number of issues causing crashes on Android devices
- Fixed an issue with auto capitalization on Android keyboards
- Fixed an issue where the GitLab SSO login button sometimes didn't appear
- Fixed an issue with link previews not appearing on some accounts
- Fixed an issue where logging out of the app didn't clear the notification badge on the homescreen icon
- Fixed an issue where interactive message buttons would not wrap to a new line
- Fixed an issue where the keyboard would sometimes overlap the text input box
- Fixed an issue where the Direct Message channel wouldn't open from the profile page
- Fixed an issue where posts would sometimes overlap
- Fixed an issue where the app sometimes hangs on logout
## v1.5.3 Release
- Release Date: February 1, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
- Fixed a login issue when connecting to servers running a Data Retention policy
## 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
View File

@@ -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'
}
}
}
}

260
Makefile
View File

@@ -1,28 +1,24 @@
.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
POD := $(shell which 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)
ios_target := $(filter-out build-ios,$(MAKECMDGOALS))
android_target := $(filter-out build-android,$(MAKECMDGOALS))
POD := $(shell command -v pod 2> /dev/null)
.npminstall: package.json
@if ! [ $(shell which npm 2> /dev/null) ]; then \
echo "npm is not installed https://npmjs.com"; \
.yarninstall: package.json
@if ! [ $(shell command -v yarn 2> /dev/null) ]; then \
@echo "yarn is not installed https://yarnpkg.com"; \
exit 1; \
fi
@echo Getting Javascript dependencies
@npm install
@yarn install --pure-lockfile
@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,17 +42,63 @@ dist/assets: $(BASE_ASSETS) $(OVERRIDE_ASSETS)
@echo "Generating app assets"
@node scripts/make-dist-assets.js
pre-run: | .npminstall .podinstall dist/assets ## Installs dependencies and assets
pre-run: | .yarninstall .podinstall dist/assets
check-style: .npminstall ## 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
@npm run check
@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 .npminstall
@rm -f .yarninstall
@rm -f .podinstall
@rm -rf dist
@rm -rf ios/build
@@ -76,158 +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|super.onBackPressed();|this.moveTaskToBack(true);|g" node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/controllers/NavigationActivity.java
@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/mattermost-redux && npm run build
@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 -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 -ef | grep -i "cli.js start" | grep -civ grep) -eq 1 ]; then \
ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | 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 which xcodebuild) ]; then \
echo "xcode is not installed"; \
exit 1; \
fi
@if ! [ $(shell which 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 which 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 which 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; \
if [ ! -z "${SIMULATOR}" ]; then \
react-native run-ios --simulator="${SIMULATOR}"; \
else \
react-native run-ios; \
fi; \
wait; \
else \
echo Running iOS app in development; \
if [ ! -z "${SIMULATOR}" ]; then \
react-native run-ios --simulator="${SIMULATOR}"; \
else \
react-native run-ios; \
fi; \
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
@npm 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:
@:

1312
NOTICE.txt

File diff suppressed because it is too large Load Diff

View File

@@ -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).

View File

@@ -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',
],
)

View File

@@ -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,26 +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") {
project.ext.sentryCli = [
logLevel: "error",
flavorAware: false
]
if (System.getenv("MM_SENTRY_ENABLED") == "true") {
apply from: "../../node_modules/react-native-sentry/sentry.gradle"
}
@@ -111,8 +95,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion 21
targetSdkVersion 23
versionCode 101
versionName "1.8.0"
versionCode 64
versionName "1.4.1"
multiDexEnabled true
ndk {
abiFilters "armeabi-v7a", "x86"
@@ -168,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'

View File

@@ -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

View File

@@ -56,22 +56,6 @@
<activity
android:name="com.reactnativenavigation.controllers.NavigationActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"/>
<activity
android:noHistory="true"
android:excludeFromRecents="true"
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>

View File

@@ -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();

View File

@@ -1,31 +1,10 @@
package com.mattermost.rnbeta;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.reactnativenavigation.controllers.SplashActivity;
public class MainActivity extends SplashActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/**
* Reference: https://stackoverflow.com/questions/7944338/resume-last-activity-when-launcher-icon-is-clicked
* 1. Open app from launcher/appDrawer
* 2. Go home
* 3. Send notification and open
* 4. It creates a new Activity and Destroys the old
* 5. Causing an unnecessary app restart
* 6. This solution short-circuits the restart
*/
if (!isTaskRoot()) {
finish();
return;
}
}
@Override
public int getSplashLayout() {
return R.layout.launch_screen;
}
@Override
public int getSplashLayout() {
return R.layout.launch_screen;
}
}

View File

@@ -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();
@@ -93,13 +82,6 @@ public class MainApplication extends NavigationApplication implements INotificat
SoLoader.init(this, /* native exopackage */ false);
}
@Override
public boolean clearHostOnActivityDestroy() {
// This solves the issue where the splash screen does not go away
// after the app is killed by the OS cause of memory or a long time in the background
return false;
}
@Override
public IPushNotification getPushNotification(Context context, Bundle bundle, AppLifecycleFacade defaultFacade, AppLaunchHelper defaultAppLaunchHelper) {
return new CustomPushNotification(

View File

@@ -27,6 +27,6 @@ public class MattermostPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList();
return Collections.emptyList();
}
}

View File

@@ -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);
}
}

View File

@@ -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());

View File

@@ -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),

View File

@@ -39,11 +39,12 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
private final BroadcastReceiver restrictionsReceiver = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
if (context != null) {
if (mVisibleActivity != null) {
// Get the current configuration bundle
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) context
.getSystemService(Context.RESTRICTIONS_SERVICE);
(RestrictionsManager) mVisibleActivity
.getSystemService(Context.RESTRICTIONS_SERVICE);
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
// Check current configuration settings, change your app's UI and
@@ -65,11 +66,11 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
MattermostManagedModule managedModule = MattermostManagedModule.getInstance();
if (managedModule != null && managedModule.isBlurAppScreenEnabled() && activity != null) {
if (managedModule != null && managedModule.isBlurAppScreenEnabled()) {
activity.getWindow().setFlags(LayoutParams.FLAG_SECURE,
LayoutParams.FLAG_SECURE);
}
if (managedConfig!= null && managedConfig.size() > 0 && activity != null) {
if (managedConfig!= null && managedConfig.size() > 0) {
activity.registerReceiver(restrictionsReceiver, restrictionsFilter);
}
}
@@ -78,11 +79,9 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
public void onActivityResumed(Activity activity) {
switchToVisible(activity);
ReactContext ctx = getRunningReactContext();
if (managedConfig != null && managedConfig.size() > 0 && ctx != null) {
if (managedConfig != null && managedConfig.size() > 0) {
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) ctx
(RestrictionsManager) activity
.getSystemService(Context.RESTRICTIONS_SERVICE);
Bundle newConfig = myRestrictionsMgr.getApplicationRestrictions();
@@ -175,15 +174,13 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
}
}
public synchronized void LoadManagedConfig(ReactContext ctx) {
if (ctx != null) {
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) ctx
.getSystemService(Context.RESTRICTIONS_SERVICE);
public synchronized void LoadManagedConfig(Activity activity) {
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) activity
.getSystemService(Context.RESTRICTIONS_SERVICE);
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
myRestrictionsMgr = null;
}
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
myRestrictionsMgr = null;
}
public synchronized Bundle getManagedConfig() {
@@ -191,10 +188,8 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
return managedConfig;
}
ReactContext ctx = getRunningReactContext();
if (ctx != null) {
LoadManagedConfig(ctx);
if (mVisibleActivity != null) {
LoadManagedConfig(mVisibleActivity);
return managedConfig;
}
@@ -203,11 +198,9 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
public void sendConfigChanged(Bundle config) {
Object result = Arguments.fromBundle(config);
ReactContext ctx = getRunningReactContext();
if (ctx != null) {
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).
emit("managedConfigDidChange", result);
}
getRunningReactContext().
getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).
emit("managedConfigDidChange", result);
}
private boolean equalBundles(Bundle one, Bundle two) {

View File

@@ -1,202 +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();
}
try {
String path = getDataColumn(context, uri, null, null);
if (path != null) {
return path;
}
} catch (Exception e) {
// do nothing and try to get a temp file
}
// 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();
File cacheDir = new File(context.getCacheDir(), "mmShare");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
tmpFile = File.createTempFile("tmp", fileName, cacheDir);
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);
}
public static void deleteTempFiles(final File dir) {
try {
if (dir.isDirectory()) {
deleteRecursive(dir);
}
} catch (Exception e) {
// do nothing
}
}
private static void deleteRecursive(File fileOrDirectory) {
if (fileOrDirectory.isDirectory())
for (File child : fileOrDirectory.listFiles())
deleteRecursive(child);
fileOrDirectory.delete();
}
}

View 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";
}
}

View File

@@ -1,204 +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);
}
private File tempFolder;
@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) {
this.clear();
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();
}
}
}
RealPathUtil.deleteTempFiles(this.tempFolder);
}
@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) {
this.tempFolder = new File(currentActivity.getCacheDir(), "mmShare");
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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -3,6 +3,9 @@
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:windowIsTranslucent">false</item>
<item name="android:windowBackground">@color/white</item>
<item name="android:colorBackground">@color/white</item>
</style>
</resources>

View File

@@ -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',
],
)

View File

@@ -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'

View File

@@ -1,4 +0,0 @@
{
"name": "Mattermost",
"displayName": "Mattermost"
}

View File

@@ -11,8 +11,8 @@ export function calculateDeviceDimensions() {
type: DeviceTypes.DEVICE_DIMENSIONS_CHANGED,
data: {
deviceHeight: height,
deviceWidth: width,
},
deviceWidth: width
}
};
}
@@ -20,7 +20,7 @@ export function connection(isOnline) {
return async (dispatch, getState) => {
dispatch({
type: DeviceTypes.CONNECTION_CHANGED,
data: isOnline,
data: isOnline
}, getState);
};
}
@@ -28,21 +28,21 @@ export function connection(isOnline) {
export function setStatusBarHeight(height = 20) {
return {
type: DeviceTypes.STATUSBAR_HEIGHT_CHANGED,
data: height,
data: height
};
}
export function setDeviceOrientation(orientation) {
return {
type: DeviceTypes.DEVICE_ORIENTATION_CHANGED,
data: orientation,
data: orientation
};
}
export function setDeviceAsTablet() {
return {
type: DeviceTypes.DEVICE_TYPE_CHANGED,
data: true,
data: true
};
}
@@ -51,5 +51,5 @@ export default {
connection,
setDeviceOrientation,
setDeviceAsTablet,
setStatusBarHeight,
setStatusBarHeight
};

View File

@@ -11,15 +11,15 @@ export function handleUpdateUserNotifyProps(notifyProps) {
const config = state.entities.general.config;
const {currentUserId} = state.entities.users;
const {interval, user_id: userId, ...otherProps} = notifyProps;
const {interval, user_id, ...otherProps} = notifyProps;
const email = notifyProps.email;
if (config.EnableEmailBatching === 'true' && email !== 'false') {
const emailInterval = [{
user_id: userId,
user_id,
category: Preferences.CATEGORY_NOTIFICATIONS,
name: Preferences.EMAIL_INTERVAL,
value: interval,
value: interval
}];
savePreferences(currentUserId, emailInterval)(dispatch, getState);

View File

@@ -1,11 +0,0 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';
export function dismissBanner(text) {
return {
type: ViewTypes.ANNOUNCEMENT_BANNER,
data: text,
};
}

View File

@@ -10,7 +10,7 @@ import {
fetchMyChannelsAndMembers,
markChannelAsRead,
selectChannel,
leaveChannel as serviceLeaveChannel,
leaveChannel as serviceLeaveChannel
} from 'mattermost-redux/actions/channels';
import {getPosts, getPostsBefore, getPostsSince, getPostThread} from 'mattermost-redux/actions/posts';
import {getFilesForPost} from 'mattermost-redux/actions/files';
@@ -19,20 +19,17 @@ 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 {getTeamByName} from 'mattermost-redux/selectors/entities/teams';
import {
getChannelByName,
getDirectChannelName,
getUserIdFromChannelName,
isDirectChannel,
isGroupChannel,
isGroupChannel
} from 'mattermost-redux/utils/channel_utils';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {getLastCreateAt} from 'mattermost-redux/utils/post_utils';
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from 'app/constants/post_textbox';
import {isDirectChannelVisible, isGroupChannelVisible} from 'app/utils/channels';
const MAX_POST_TRIES = 3;
@@ -43,18 +40,6 @@ export function loadChannelsIfNecessary(teamId) {
};
}
export function loadChannelsByTeamName(teamName) {
return async (dispatch, getState) => {
const state = getState();
const {currentTeamId} = state.entities.teams;
const team = getTeamByName(state, teamName);
if (team && team.id !== currentTeamId) {
await dispatch(fetchMyChannelsAndMembers(team.id));
}
};
}
export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
return async (dispatch, getState) => {
const state = getState();
@@ -73,7 +58,7 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
user_id: currentUserId,
category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
name,
value: 'true',
value: 'true'
};
}
@@ -145,7 +130,7 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
actions.push({
type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL,
data: {user_id: members[i]},
id: channel.id,
id: channel.id
});
}
}
@@ -164,15 +149,10 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
const time = Date.now();
let loadMorePostsVisible = true;
let received;
if (!postsIds || postsIds.length < ViewTypes.POST_VISIBILITY_CHUNK_SIZE) {
// Get the first page of posts if it appears we haven't gotten it yet, like the webapp
received = await retryGetPostsAction(getPosts(channelId), dispatch, getState);
if (received) {
loadMorePostsVisible = received.order.length >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
}
} else {
const {lastConnectAt} = state.device.websocket;
const lastGetPosts = state.views.channel.lastGetPosts[channelId];
@@ -189,21 +169,15 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
}
received = await retryGetPostsAction(getPostsSince(channelId, since), dispatch, getState);
if (received) {
loadMorePostsVisible = postsIds.length + received.order.length >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
}
}
if (received) {
dispatch({
type: ViewTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME,
channelId,
time,
time
});
}
dispatch(setLoadMorePostsVisible(loadMorePostsVisible));
};
}
@@ -292,23 +266,20 @@ export function handleSelectChannel(channelId) {
return async (dispatch, getState) => {
const {currentTeamId} = getState().entities.teams;
dispatch(setLoadMorePostsVisible(true));
loadPostsIfNecessaryWithRetry(channelId)(dispatch, getState);
selectChannel(channelId)(dispatch, getState);
dispatch(batchActions([
{
type: ViewTypes.SET_INITIAL_POST_VISIBILITY,
data: channelId,
data: channelId
},
setChannelLoading(false),
{
type: ViewTypes.SET_LAST_CHANNEL_FOR_TEAM,
teamId: currentTeamId,
channelId,
},
]));
channelId
}
]), 'BATCH_CHANNEL_LOADED');
};
}
@@ -317,19 +288,59 @@ export function handlePostDraftChanged(channelId, draft) {
dispatch({
type: ViewTypes.POST_DRAFT_CHANGED,
channelId,
draft,
draft
}, getState);
};
}
export function handlePostDraftSelectionChanged(channelId, cursorPosition) {
return {
type: ViewTypes.POST_DRAFT_SELECTION_CHANGED,
channelId,
cursorPosition
};
}
export function insertToDraft(value) {
return (dispatch, getState) => {
const state = getState();
const channelId = getCurrentChannelId(state);
const threadId = state.entities.posts.selectedPostId;
const insertEvent = threadId ? INSERT_TO_COMMENT : INSERT_TO_DRAFT;
let draft;
let cursorPosition;
let action;
if (state.views.thread.drafts[threadId]) {
const threadDraft = state.views.thread.drafts[threadId];
draft = threadDraft.draft;
cursorPosition = threadDraft.cursorPosition;
action = {
type: ViewTypes.COMMENT_DRAFT_CHANGED,
rootId: threadId
};
} else if (state.views.channel.drafts[channelId]) {
const channelDraft = state.views.channel.drafts[channelId];
draft = channelDraft.draft;
cursorPosition = channelDraft.cursorPosition;
action = {
type: ViewTypes.POST_DRAFT_CHANGED,
channelId
};
}
EventEmitter.emit(insertEvent, value);
let nextDraft = `${value}`;
if (cursorPosition > 0) {
const beginning = draft.slice(0, cursorPosition);
const end = draft.slice(cursorPosition);
nextDraft = `${beginning}${value}${end}`;
}
if (action && nextDraft !== draft) {
dispatch({
...action,
draft: nextDraft
});
}
};
}
@@ -342,7 +353,7 @@ export function toggleDMChannel(otherUserId, visible) {
user_id: currentUserId,
category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
name: otherUserId,
value: visible,
value: visible
}];
savePreferences(currentUserId, dm)(dispatch, getState);
@@ -358,7 +369,7 @@ export function toggleGMChannel(channelId, visible) {
user_id: currentUserId,
category: Preferences.CATEGORY_GROUP_CHANNEL_SHOW,
name: channelId,
value: visible,
value: visible
}];
savePreferences(currentUserId, gm)(dispatch, getState);
@@ -400,51 +411,39 @@ export function refreshChannelWithRetry(channelId) {
export function leaveChannel(channel, reset = false) {
return async (dispatch, getState) => {
const state = getState();
const {currentChannelId} = state.entities.channels;
const {currentTeamId} = state.entities.teams;
dispatch({
type: ViewTypes.REMOVE_LAST_CHANNEL_FOR_TEAM,
data: {
teamId: currentTeamId,
channelId: channel.id,
},
});
if (channel.id === currentChannelId || reset) {
await dispatch(selectInitialChannel(currentTeamId));
}
const {currentTeamId} = getState().entities.teams;
await serviceLeaveChannel(channel.id)(dispatch, getState);
if (channel.isCurrent || reset) {
await selectInitialChannel(currentTeamId)(dispatch, getState);
}
};
}
export function setChannelLoading(loading = true) {
return {
type: ViewTypes.SET_CHANNEL_LOADER,
loading,
loading
};
}
export function setChannelRefreshing(loading = true) {
return {
type: ViewTypes.SET_CHANNEL_REFRESHING,
loading,
loading
};
}
export function setChannelRetryFailed(failed = true) {
return {
type: ViewTypes.SET_CHANNEL_RETRY_FAILED,
failed,
failed
};
}
export function setChannelDisplayName(displayName) {
return {
type: ViewTypes.SET_CHANNEL_DISPLAY_NAME,
displayName,
displayName
};
}
@@ -461,16 +460,16 @@ export function increasePostVisibility(channelId, focusedPostId) {
// Check if we already have the posts that we want to show
if (!focusedPostId) {
const postsInChannel = state.entities.posts.postsInChannel[channelId] || [];
const loadedPostCount = postsInChannel.length;
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(batchActions([
doIncreasePostVisibility(channelId),
setLoadMorePostsVisible(true),
]));
dispatch({
type: ViewTypes.INCREASE_POST_VISIBILITY,
data: channelId,
amount: ViewTypes.POST_VISIBILITY_CHUNK_SIZE
});
return;
}
@@ -479,49 +478,33 @@ export function increasePostVisibility(channelId, focusedPostId) {
dispatch({
type: ViewTypes.LOADING_POSTS,
data: true,
channelId,
channelId
});
const pageSize = ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
const page = Math.floor(currentPostVisibility / pageSize);
const page = Math.floor(currentPostVisibility / ViewTypes.POST_VISIBILITY_CHUNK_SIZE);
let result;
if (focusedPostId) {
result = await getPostsBefore(channelId, focusedPostId, page, pageSize)(dispatch, getState);
result = await getPostsBefore(channelId, focusedPostId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
} else {
result = await getPosts(channelId, page, pageSize)(dispatch, getState);
result = await getPosts(channelId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
}
const actions = [{
type: ViewTypes.LOADING_POSTS,
data: false,
channelId,
}];
const posts = result.data;
if (posts) {
// make sure to increment the posts visibility
// only if we got results
actions.push(doIncreasePostVisibility(channelId));
actions.push(setLoadMorePostsVisible(posts.order.length >= pageSize));
dispatch({
type: ViewTypes.INCREASE_POST_VISIBILITY,
data: channelId,
amount: ViewTypes.POST_VISIBILITY_CHUNK_SIZE
});
}
dispatch(batchActions(actions));
};
}
function doIncreasePostVisibility(channelId) {
return {
type: ViewTypes.INCREASE_POST_VISIBILITY,
data: channelId,
amount: ViewTypes.POST_VISIBILITY_CHUNK_SIZE,
};
}
function setLoadMorePostsVisible(visible) {
return {
type: ViewTypes.SET_LOAD_MORE_POSTS_VISIBLE,
data: visible,
dispatch({
type: ViewTypes.LOADING_POSTS,
data: false,
channelId
});
};
}

View File

@@ -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
}
};
}

View File

@@ -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
}
};
}

View File

@@ -5,6 +5,6 @@ import {ViewTypes} from 'app/constants';
export function setLastUpgradeCheck() {
return {
type: ViewTypes.SET_LAST_UPGRADE_CHECK,
type: ViewTypes.SET_LAST_UPGRADE_CHECK
};
}

View File

@@ -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);
};
}

View File

@@ -18,7 +18,7 @@ export function handleCreateChannel(displayName, purpose, header, type) {
display_name: displayName,
purpose,
header,
type,
type
};
const {data} = await createChannel(channel, currentUserId)(dispatch, getState);

View File

@@ -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);
};
}

View File

@@ -1,44 +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,
};
}
export function incrementEmojiPickerPage() {
return async (dispatch) => {
dispatch({
type: ViewTypes.INCREMENT_EMOJI_PICKER_PAGE,
});
return {data: true};
dispatch(addReaction(lastPostId, emoji));
};
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';
export function addFileToFetchCache(url) {
return {
type: ViewTypes.ADD_FILE_TO_FETCH_CACHE,
url
};
}

View File

@@ -1,55 +1,60 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {FileTypes} from 'mattermost-redux/action_types';
import FormData from 'form-data';
import {Platform} from 'react-native';
import {uploadFile} from 'mattermost-redux/actions/files';
import {lookupMimeType, parseClientIdsFromFormData} from 'mattermost-redux/utils/file_utils';
import {generateId} from 'app/utils/file';
import {ViewTypes} from 'app/constants';
import {buildFileUploadData, generateId} from 'app/utils/file';
export function initUploadFiles(files, rootId) {
return (dispatch, getState) => {
export function handleUploadFiles(files, rootId) {
return async (dispatch, getState) => {
const state = getState();
const channelId = state.entities.channels.currentChannelId;
const formData = new FormData();
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.type,
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);
});
let formBoundary;
if (Platform.os === 'ios') {
formBoundary = '--mobile.client.file.upload';
}
dispatch({
type: ViewTypes.SET_TEMP_UPLOAD_FILES_FOR_POST_DRAFT,
clientIds,
channelId,
rootId,
rootId
});
};
}
export function uploadFailed(clientIds, channelId, rootId, error) {
return {
type: FileTypes.UPLOAD_FILES_FAILURE,
clientIds,
channelId,
rootId,
error,
};
}
export function uploadComplete(data, channelId, rootId) {
return {
type: FileTypes.RECEIVED_UPLOAD_FILES,
data,
channelId,
rootId,
await uploadFile(channelId, rootId, parseClientIdsFromFormData(formData), formData, formBoundary)(dispatch, getState);
};
}
@@ -58,13 +63,31 @@ export function retryFileUpload(file, rootId) {
const state = getState();
const channelId = state.entities.channels.currentChannelId;
const formData = new FormData();
const fileData = {
uri: file.localPath,
name: file.name,
type: file.type
};
formData.append('files', fileData);
formData.append('channel_id', channelId);
formData.append('client_ids', file.clientId);
let formBoundary;
if (Platform.os === 'ios') {
formBoundary = '--mobile.client.file.upload';
}
dispatch({
type: ViewTypes.RETRY_UPLOAD_FILE_FOR_POST,
clientId: file.clientId,
channelId,
rootId,
rootId
});
await uploadFile(channelId, rootId, [file.clientId], formData, formBoundary)(dispatch, getState);
};
}
@@ -72,15 +95,7 @@ export function handleClearFiles(channelId, rootId) {
return {
type: ViewTypes.CLEAR_FILES_FOR_POST_DRAFT,
channelId,
rootId,
};
}
export function handleClearFailedFiles(channelId, rootId) {
return {
type: ViewTypes.CLEAR_FAILED_FILES_FOR_POST_DRAFT,
channelId,
rootId,
rootId
};
}
@@ -89,7 +104,7 @@ export function handleRemoveFile(clientId, channelId, rootId) {
type: ViewTypes.REMOVE_FILE_FROM_POST_DRAFT,
clientId,
channelId,
rootId,
rootId
};
}
@@ -97,6 +112,6 @@ export function handleRemoveLastFile(channelId, rootId) {
return {
type: ViewTypes.REMOVE_LAST_FILE_FROM_POST_DRAFT,
channelId,
rootId,
rootId
};
}

View File

@@ -1,9 +1,7 @@
// 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 {Client4} from 'mattermost-redux/client';
import {Client, Client4} from 'mattermost-redux/client';
import {ViewTypes} from 'app/constants';
@@ -11,7 +9,7 @@ export function handleLoginIdChanged(loginId) {
return async (dispatch, getState) => {
dispatch({
type: ViewTypes.LOGIN_ID_CHANGED,
loginId,
loginId
}, getState);
};
}
@@ -20,31 +18,25 @@ export function handlePasswordChanged(password) {
return async (dispatch, getState) => {
dispatch({
type: ViewTypes.PASSWORD_CHANGED,
password,
password
}, getState);
};
}
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: {
url,
token,
},
token
}
}, getState);
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
license.IsLicensed === 'true' && license.DataRetention === 'true') {
getDataRetentionPolicy()(dispatch, getState);
} else {
dispatch({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
}
Client.setToken(token);
Client.setUrl(url);
return true;
};
@@ -73,5 +65,5 @@ export default {
handleLoginIdChanged,
handlePasswordChanged,
handleSuccessfulLogin,
getSession,
getSession
};

View File

@@ -1,39 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {Posts} from 'mattermost-redux/constants';
import {PostTypes} from 'mattermost-redux/action_types';
import {generateId} from 'app/utils/file';
export function sendAddToChannelEphemeralPost(user, addedUsername, message, channelId, postRootId = '') {
return async (dispatch) => {
const timestamp = Date.now();
const post = {
id: generateId(),
user_id: user.id,
channel_id: channelId,
message,
type: Posts.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL,
create_at: timestamp,
update_at: timestamp,
root_id: postRootId,
parent_id: postRootId,
props: {
username: user.username,
addedUsername,
},
};
dispatch({
type: PostTypes.RECEIVED_POSTS,
data: {
order: [],
posts: {
[post.id]: post,
},
},
channelId,
});
};
}

View File

@@ -1,42 +1,26 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {GeneralTypes, PostTypes} from 'mattermost-redux/action_types';
import {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,
retryGetPostsAction,
retryGetPostsAction
} from 'app/actions/views/channel';
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),
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};
};
}
@@ -56,7 +40,7 @@ export function loadFromPushNotification(notification) {
if (teamId && (!teams[teamId] || !myTeamMembers[teamId])) {
await Promise.all([
getMyTeams()(dispatch, getState),
getMyTeamMembers()(dispatch, getState),
getMyTeamMembers()(dispatch, getState)
]);
}
@@ -68,11 +52,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);
}
@@ -84,7 +66,7 @@ export function purgeOfflineStore() {
}
export function createPost(post) {
return (dispatch, getState) => {
return async (dispatch, getState) => {
const state = getState();
const currentUserId = state.entities.users.currentUserId;
@@ -95,34 +77,31 @@ export function createPost(post) {
...post,
pending_post_id: pendingPostId,
create_at: timestamp,
update_at: timestamp,
update_at: timestamp
};
return Client4.createPost({...newPost, create_at: 0}).then((payload) => {
try {
const payload = Client4.createPost({...newPost, create_at: 0});
dispatch({
type: PostTypes.RECEIVED_POSTS,
data: {
order: [],
posts: {
[payload.id]: payload,
},
[payload.id]: payload
}
},
channelId: payload.channel_id,
channelId: payload.channel_id
});
});
};
}
} catch (error) {
return {error};
}
export function recordLoadTime(screenName, category) {
return async (dispatch, getState) => {
const {currentUserId} = getState().entities.users;
recordTime(screenName, category, currentUserId);
return {data: true};
};
}
export default {
loadConfigAndLicense,
loadFromPushNotification,
purgeOfflineStore,
purgeOfflineStore
};

View File

@@ -7,7 +7,7 @@ export function handleSearchDraftChanged(text) {
return async (dispatch, getState) => {
dispatch({
type: ViewTypes.SEARCH_DRAFT_CHANGED,
text,
text
}, getState);
};
}

View File

@@ -1,21 +1,17 @@
// 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);
};
}
export default {
handleServerUrlChanged,
handleServerUrlChanged
};

View File

@@ -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';
@@ -22,7 +22,7 @@ export function handleTeamChange(teamId, selectChannel = true) {
const actions = [
setChannelDisplayName(''),
{type: TeamTypes.SELECT_TEAM, data: teamId},
{type: TeamTypes.SELECT_TEAM, data: teamId}
];
if (selectChannel) {
@@ -31,11 +31,11 @@ export function handleTeamChange(teamId, selectChannel = true) {
const lastChannels = state.views.team.lastChannelForTeam[teamId] || [];
const lastChannelId = lastChannels[0] || '';
const currentChannelId = getCurrentChannelId(state);
markChannelAsViewed(currentChannelId)(dispatch, getState);
viewChannel(currentChannelId)(dispatch, getState);
markChannelAsRead(lastChannelId, currentChannelId)(dispatch, getState);
}
dispatch(batchActions(actions, 'BATCH_SELECT_TEAM'), getState);
dispatch(batchActions(actions), getState);
};
}
@@ -55,5 +55,5 @@ export function selectFirstAvailableTeam() {
export default {
handleTeamChange,
selectFirstAvailableTeam,
selectFirstAvailableTeam
};

View File

@@ -8,7 +8,7 @@ export function handleCommentDraftChanged(rootId, draft) {
dispatch({
type: ViewTypes.COMMENT_DRAFT_CHANGED,
rootId,
draft,
draft
}, getState);
};
}
@@ -17,6 +17,6 @@ export function handleCommentDraftSelectionChanged(rootId, cursorPosition) {
return {
type: ViewTypes.COMMENT_DRAFT_SELECTION_CHANGED,
rootId,
cursorPosition,
cursorPosition
};
}

View File

@@ -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);
}
};
}

View File

@@ -1,144 +0,0 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Alert,
Animated,
StyleSheet,
Text,
TouchableOpacity,
} from 'react-native';
import {intlShape} from 'react-intl';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
const {View: AnimatedView} = Animated;
export default class AnnouncementBanner extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
dismissBanner: PropTypes.func.isRequired,
}).isRequired,
allowDismissal: PropTypes.bool,
bannerColor: PropTypes.string,
bannerDismissed: PropTypes.bool,
bannerEnabled: PropTypes.bool,
bannerText: PropTypes.string,
bannerTextColor: PropTypes.string,
};
static contextTypes = {
intl: intlShape,
};
state = {
bannerHeight: new Animated.Value(0),
};
componentWillMount() {
const {bannerDismissed, bannerEnabled, bannerText} = this.props;
const showBanner = bannerEnabled && !bannerDismissed && Boolean(bannerText);
this.toggleBanner(showBanner);
}
componentWillReceiveProps(nextProps) {
if (this.props.bannerText !== nextProps.bannerText ||
this.props.bannerEnabled !== nextProps.bannerEnabled ||
this.props.bannerDismissed !== nextProps.bannerDismissed) {
const showBanner = nextProps.bannerEnabled && !nextProps.bannerDismissed && Boolean(nextProps.bannerText);
this.toggleBanner(showBanner);
}
}
handleDismiss = () => {
const {actions, bannerText} = this.props;
actions.dismissBanner(bannerText);
};
handlePress = () => {
const {formatMessage} = this.context.intl;
const options = [{
text: formatMessage({id: 'mobile.announcement_banner.ok', defaultMessage: 'OK'}),
}];
if (this.props.allowDismissal) {
options.push({
text: formatMessage({id: 'mobile.announcement_banner.dismiss', defaultMessage: 'Dismiss'}),
onPress: this.handleDismiss,
});
}
Alert.alert(
formatMessage({id: 'mobile.announcement_banner.title', defaultMessage: 'Announcement'}),
this.props.bannerText,
options,
{cancelable: false}
);
};
toggleBanner = (show = true) => {
const value = show ? 38 : 0;
Animated.timing(this.state.bannerHeight, {
toValue: value,
duration: 350,
}).start();
};
render() {
const {bannerHeight} = this.state;
const bannerStyle = {
backgroundColor: this.props.bannerColor,
height: bannerHeight,
};
const bannerTextStyle = {
color: this.props.bannerTextColor,
};
return (
<AnimatedView
style={[style.bannerContainer, bannerStyle]}
>
<TouchableOpacity
onPress={this.handlePress}
style={style.wrapper}
>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={[style.bannerText, bannerTextStyle]}
>
{this.props.bannerText}
</Text>
<MaterialIcons
color={this.props.bannerTextColor}
name='info'
size={16}
/>
</TouchableOpacity>
</AnimatedView>
);
}
}
const style = StyleSheet.create({
bannerContainer: {
paddingHorizontal: 10,
position: 'absolute',
top: 0,
overflow: 'hidden',
width: '100%',
},
wrapper: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
},
bannerText: {
flex: 1,
fontSize: 14,
marginRight: 5,
},
});

View File

@@ -1,36 +0,0 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {dismissBanner} from 'app/actions/views/announcement';
import AnnouncementBanner from './announcement_banner';
function mapStateToProps(state) {
const config = getConfig(state);
const license = getLicense(state);
const {announcement} = state.views;
return {
allowDismissal: config.AllowBannerDismissal === 'true',
bannerColor: config.BannerColor,
bannerDismissed: config.BannerText === announcement,
bannerEnabled: config.EnableBanner === 'true' && license.IsLicensed === 'true',
bannerText: config.BannerText,
bannerTextColor: config.BannerTextColor,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
dismissBanner,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AnnouncementBanner);

View File

@@ -5,14 +5,14 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import Svg, {
G,
Path,
Path
} from 'react-native-svg';
export default class AppIcon extends PureComponent {
export default class AwayStatus extends PureComponent {
static propTypes = {
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
color: PropTypes.string.isRequired,
color: PropTypes.string.isRequired
};
render() {

View File

@@ -3,82 +3,74 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Clipboard, Platform, Text} from 'react-native';
import {intlShape} from 'react-intl';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
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,
teammateNameDisplay: PropTypes.string,
theme: PropTypes.object.isRequired,
usersByUsername: PropTypes.object.isRequired,
};
static contextTypes = {
intl: intlShape,
usersByUsername: PropTypes.object.isRequired
};
constructor(props) {
super(props);
const user = this.getUserDetailsFromMentionName(props);
const userDetails = this.getUserDetailsFromMentionName(props);
this.state = {
user,
username: userDetails.username,
id: userDetails.id
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.mentionName !== this.props.mentionName || nextProps.usersByUsername !== this.props.usersByUsername) {
const user = this.getUserDetailsFromMentionName(nextProps);
const userDetails = this.getUserDetailsFromMentionName(nextProps);
this.setState({
user,
username: userDetails.username,
id: userDetails.id
});
}
}
goToUserProfile = () => {
const {navigator, theme} = this.props;
const {intl} = this.context;
const options = {
const {intl, navigator, theme} = this.props;
navigator.push({
screen: 'UserProfile',
title: intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}),
animated: true,
backButtonTitle: '',
passProps: {
userId: this.state.user.id,
userId: this.state.id
},
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
};
if (Platform.OS === 'ios') {
navigator.push(options);
} else {
navigator.showModal(options);
}
screenBackgroundColor: theme.centerChannelBg
}
});
};
getUserDetailsFromMentionName(props) {
let mentionName = props.mentionName;
while (mentionName.length > 0) {
if (props.usersByUsername.hasOwnProperty(mentionName)) {
return props.usersByUsername[mentionName];
if (props.usersByUsername[mentionName]) {
const user = props.usersByUsername[mentionName];
return {
username: user.username,
id: user.id
};
}
// Repeatedly trim off trailing punctuation in case this is at the end of a sentence
@@ -90,55 +82,32 @@ export default class AtMention extends React.PureComponent {
}
return {
username: '',
username: ''
};
}
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, teammateNameDisplay, textStyle} = this.props;
const {user} = this.state;
const {isSearchResult, mentionName, mentionStyle, onPostPress, textStyle} = this.props;
const username = this.state.username;
if (!user.username) {
if (!username) {
return <Text style={textStyle}>{'@' + mentionName}</Text>;
}
const suffix = this.props.mentionName.substring(user.username.length);
const suffix = this.props.mentionName.substring(username.length);
return (
<Text
style={textStyle}
onPress={isSearchResult ? onPostPress : this.goToUserProfile}
onLongPress={this.handleLongPress}
>
<Text style={mentionStyle}>
{'@' + displayUsername(user, teammateNameDisplay)}
{'@' + username}
</Text>
{suffix}
</Text>
);
}
}
export default injectIntl(AtMention);

View File

@@ -5,15 +5,15 @@ import {connect} from 'react-redux';
import {getUsersByUsername} from 'mattermost-redux/selectors/entities/users';
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
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),
teammateNameDisplay: getTeammateNameDisplaySetting(state),
...ownProps
};
}

View File

@@ -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';
@@ -17,7 +16,7 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class AtMention extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
autocompleteUsers: PropTypes.func.isRequired,
autocompleteUsers: PropTypes.func.isRequired
}).isRequired,
currentChannelId: PropTypes.string,
currentTeamId: PropTypes.string.isRequired,
@@ -25,28 +24,26 @@ 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,
requestStatus: PropTypes.string.isRequired,
teamMembers: PropTypes.array,
theme: PropTypes.object.isRequired,
value: PropTypes.string,
value: PropTypes.string
};
static defaultProps = {
defaultChannel: {},
isSearch: false,
value: '',
value: ''
};
constructor(props) {
super(props);
this.state = {
sections: [],
sections: []
};
}
@@ -56,11 +53,8 @@ export default class AtMention extends PureComponent {
// if the term changes but is null or the mention has been completed we render this component as null
this.setState({
mentionComplete: false,
sections: [],
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
@@ -84,7 +78,7 @@ export default class AtMention extends PureComponent {
id: 'mobile.suggestion.members',
defaultMessage: 'Members',
data: teamMembers,
key: 'teamMembers',
key: 'teamMembers'
});
} else {
if (inChannel.length) {
@@ -92,7 +86,7 @@ export default class AtMention extends PureComponent {
id: 'suggestion.mention.members',
defaultMessage: 'Channel Members',
data: inChannel,
key: 'inChannel',
key: 'inChannel'
});
}
@@ -102,7 +96,7 @@ export default class AtMention extends PureComponent {
defaultMessage: 'Special Mentions',
data: this.getSpecialMentions(),
key: 'special',
renderItem: this.renderSpecialMentions,
renderItem: this.renderSpecialMentions
});
}
@@ -111,16 +105,14 @@ export default class AtMention extends PureComponent {
id: 'suggestion.mention.nonmembers',
defaultMessage: 'Not in Channel',
data: outChannel,
key: 'outChannel',
key: 'outChannel'
});
}
}
this.setState({
sections,
sections
});
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
}
}
@@ -134,16 +126,16 @@ export default class AtMention extends PureComponent {
id: 'suggestion.mention.all',
defaultMessage: 'Notifies everyone in the channel, use in {townsquare} to notify the whole team',
values: {
townsquare: this.props.defaultChannel.display_name,
},
townsquare: this.props.defaultChannel.display_name
}
}, {
completeHandle: 'channel',
id: 'suggestion.mention.channel',
defaultMessage: 'Notifies everyone in the channel',
defaultMessage: 'Notifies everyone in the channel'
}, {
completeHandle: 'here',
id: 'suggestion.mention.here',
defaultMessage: 'Notifies everyone in the channel and online',
defaultMessage: 'Notifies everyone in the channel and online'
}];
};
@@ -166,7 +158,7 @@ export default class AtMention extends PureComponent {
completedDraft += value.substring(cursorPosition);
}
onChangeText(completedDraft);
onChangeText(completedDraft, true);
this.setState({mentionComplete: 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}
/>
);
@@ -232,10 +223,10 @@ export default class AtMention extends PureComponent {
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
listView: {
backgroundColor: theme.centerChannelBg,
backgroundColor: theme.centerChannelBg
},
search: {
minHeight: 125,
},
height: 250
}
};
});

View File

@@ -12,7 +12,7 @@ import {
filterMembersInChannel,
filterMembersNotInChannel,
filterMembersInCurrentTeam,
getMatchTermForAtMention,
getMatchTermForAtMention
} from 'app/selectors/autocomplete';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
@@ -44,15 +44,15 @@ function mapStateToProps(state, ownProps) {
inChannel,
outChannel,
requestStatus: state.requests.users.autocompleteUsers.status,
theme: getTheme(state),
theme: getTheme(state)
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
autocompleteUsers,
}, dispatch),
autocompleteUsers
}, dispatch)
};
}

View File

@@ -6,11 +6,11 @@ import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity,
View,
View
} 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 = {
@@ -19,7 +19,7 @@ export default class AtMentionItem extends PureComponent {
onPress: PropTypes.func.isRequired,
userId: PropTypes.string.isRequired,
username: PropTypes.string,
theme: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
completeMention = () => {
@@ -33,7 +33,7 @@ export default class AtMentionItem extends PureComponent {
lastName,
userId,
username,
theme,
theme
} = this.props;
const style = getStyleFromTheme(theme);
@@ -60,7 +60,6 @@ export default class AtMentionItem extends PureComponent {
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
@@ -68,20 +67,26 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
flexDirection: 'row',
alignItems: 'center',
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,
width: 20,
alignItems: 'center',
justifyContent: 'center',
justifyContent: 'center'
},
rowUsername: {
fontSize: 13,
color: theme.centerChannelColor,
color: theme.centerChannelColor
},
rowFullname: {
color: theme.centerChannelColor,
opacity: 0.6,
},
opacity: 0.6
}
};
});

View File

@@ -17,6 +17,7 @@ function mapStateToProps(state, ownProps) {
lastName: user.last_name,
username: user.username,
theme: getTheme(state),
...ownProps
};
}

View File

@@ -1,172 +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 = {
cursorPosition: PropTypes.number.isRequired,
deviceHeight: PropTypes.number,
onChangeText: PropTypes.func.isRequired,
rootId: PropTypes.string,
isSearch: PropTypes.bool,
theme: PropTypes.object.isRequired,
value: PropTypes.string,
};
static defaultProps = {
isSearch: false,
cursorPosition: 0,
};
state = {
atMentionCount: 0,
channelMentionCount: 0,
emojiCount: 0,
commandCount: 0,
keyboardOffset: 0,
};
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}
onResultCountChange={this.handleAtMentionCountChange}
{...this.props}
/>
<ChannelMention
listHeight={listHeight}
onResultCountChange={this.handleChannelMentionCountChange}
{...this.props}
/>
<EmojiSuggestion
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,
},
}),
},
};
});

View File

@@ -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),
},
};
});

View File

@@ -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);

View File

@@ -12,7 +12,7 @@ export default class AutocompleteSectionHeader extends PureComponent {
static propTypes = {
defaultMessage: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
render() {
@@ -41,14 +41,18 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderTopWidth: 1,
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,
color: changeOpacity(theme.centerChannelColor, 0.7),
paddingVertical: 7,
paddingVertical: 7
},
sectionWrapper: {
backgroundColor: theme.centerChannelBg,
},
backgroundColor: theme.centerChannelBg
}
};
});

View File

@@ -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';
@@ -16,34 +15,32 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class ChannelMention extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
searchChannels: PropTypes.func.isRequired,
searchChannels: PropTypes.func.isRequired
}).isRequired,
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,
privateChannels: PropTypes.array,
publicChannels: PropTypes.array,
requestStatus: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
value: PropTypes.string,
value: PropTypes.string
};
static defaultProps = {
isSearch: false,
value: '',
value: ''
};
constructor(props) {
super(props);
this.state = {
sections: [],
sections: []
};
}
@@ -54,11 +51,8 @@ export default class ChannelMention extends PureComponent {
// if the term changes but is null or the mention has been completed we render this component as null
this.setState({
mentionComplete: false,
sections: [],
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
@@ -83,7 +77,7 @@ export default class ChannelMention extends PureComponent {
id: 'suggestion.search.public',
defaultMessage: 'Public Channels',
data: publicChannels,
key: 'publicChannels',
key: 'publicChannels'
});
}
@@ -92,7 +86,7 @@ export default class ChannelMention extends PureComponent {
id: 'suggestion.search.private',
defaultMessage: 'Private Channels',
data: privateChannels,
key: 'privateChannels',
key: 'privateChannels'
});
}
} else {
@@ -101,7 +95,7 @@ export default class ChannelMention extends PureComponent {
id: 'suggestion.mention.channels',
defaultMessage: 'My Channels',
data: myChannels,
key: 'myChannels',
key: 'myChannels'
});
}
@@ -110,16 +104,14 @@ export default class ChannelMention extends PureComponent {
id: 'suggestion.mention.morechannels',
defaultMessage: 'Other Channels',
data: otherChannels,
key: 'otherChannels',
key: 'otherChannels'
});
}
}
this.setState({
sections,
sections
});
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
}
}
@@ -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}
/>
);
@@ -196,10 +187,10 @@ export default class ChannelMention extends PureComponent {
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
listView: {
backgroundColor: theme.centerChannelBg,
backgroundColor: theme.centerChannelBg
},
search: {
minHeight: 125,
},
height: 250
}
};
});

View File

@@ -12,7 +12,7 @@ import {
filterOtherChannels,
filterPublicChannels,
filterPrivateChannels,
getMatchTermForChannelMention,
getMatchTermForChannelMention
} from 'app/selectors/autocomplete';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
@@ -37,6 +37,7 @@ function mapStateToProps(state, ownProps) {
}
return {
...ownProps,
myChannels,
otherChannels,
publicChannels,
@@ -44,15 +45,15 @@ function mapStateToProps(state, ownProps) {
currentTeamId: getCurrentTeamId(state),
matchTerm,
requestStatus: state.requests.channels.getChannels.status,
theme: getTheme(state),
theme: getTheme(state)
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
searchChannels,
}, dispatch),
searchChannels
}, dispatch)
};
}

View File

@@ -5,10 +5,10 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity,
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 = {
@@ -16,7 +16,7 @@ export default class ChannelMentionItem extends PureComponent {
displayName: PropTypes.string,
name: PropTypes.string,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
completeMention = () => {
@@ -29,7 +29,7 @@ export default class ChannelMentionItem extends PureComponent {
channelId,
displayName,
name,
theme,
theme
} = this.props;
const style = getStyleFromTheme(theme);
@@ -46,7 +46,6 @@ export default class ChannelMentionItem extends PureComponent {
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
@@ -54,14 +53,20 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
flexDirection: 'row',
alignItems: 'center',
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,
color: theme.centerChannelColor
},
rowName: {
color: theme.centerChannelColor,
opacity: 0.6,
},
opacity: 0.6
}
};
});

View File

@@ -16,6 +16,7 @@ function mapStateToProps(state, ownProps) {
displayName: channel.display_name,
name: channel.name,
theme: getTheme(state),
...ownProps
};
}

View File

@@ -7,122 +7,71 @@ import {
FlatList,
Text,
TouchableOpacity,
View,
View
} from 'react-native';
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
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 = {
actions: PropTypes.shape({
addReactionToLatestPost: PropTypes.func.isRequired,
autocompleteCustomEmojis: PropTypes.func.isRequired,
addReactionToLatestPost: PropTypes.func.isRequired
}).isRequired,
cursorPosition: PropTypes.number,
emojis: PropTypes.array.isRequired,
isSearch: PropTypes.bool,
fuse: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
rootId: PropTypes.string,
value: PropTypes.string,
serverVersion: PropTypes.string,
value: PropTypes.string
};
static defaultProps = {
defaultChannel: {},
value: '',
value: ''
};
state = {
active: false,
dataSource: [],
dataSource: []
};
constructor(props) {
super(props);
this.matchTerm = '';
}
componentWillReceiveProps(nextProps) {
if (nextProps.isSearch) {
return;
}
const regex = EMOJI_REGEX;
const match = nextProps.value.substring(0, nextProps.cursorPosition).match(regex);
if (!match || this.state.emojiComplete) {
this.setState({
active: false,
emojiComplete: false,
matchTerm: null,
emojiComplete: false
});
this.props.onResultCountChange(0);
return;
}
const oldMatchTerm = this.matchTerm;
this.matchTerm = match[3] || '';
// If we're server version 4.7 or higher
if (isMinimumServerVersion(this.props.serverVersion, 4, 7)) {
if (this.matchTerm !== oldMatchTerm && this.matchTerm.length) {
this.props.actions.autocompleteCustomEmojis(this.matchTerm);
return;
}
if (this.matchTerm.length) {
this.handleFuzzySearch(this.matchTerm, nextProps);
} else {
const initialEmojis = [...nextProps.emojis];
initialEmojis.splice(0, 300);
const data = initialEmojis.sort();
this.setEmojiData(data);
}
return;
const matchTerm = match[3];
if (matchTerm !== this.state.matchTerm) {
this.setState({
matchTerm
});
}
// If we're server version 4.6 or lower
if (this.matchTerm !== oldMatchTerm) {
this.handleFuzzySearch(this.matchTerm, nextProps);
} else if (!this.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,
dataSource: data,
active: data.length,
dataSource: data
});
this.props.onResultCountChange(data.length);
};
}
completeSuggestion = (emoji) => {
const {actions, cursorPosition, onChangeText, value, rootId} = this.props;
@@ -132,7 +81,7 @@ export default class EmojiSuggestion extends Component {
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);
@@ -143,7 +92,7 @@ export default class EmojiSuggestion extends Component {
this.setState({
active: false,
emojiComplete: true,
emojiComplete: true
});
};
@@ -187,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}
/>
@@ -198,15 +146,15 @@ export default class EmojiSuggestion extends Component {
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
emoji: {
marginRight: 5,
marginRight: 5
},
emojiName: {
fontSize: 13,
color: theme.centerChannelColor,
color: theme.centerChannelColor
},
listView: {
flex: 1,
backgroundColor: theme.centerChannelBg,
backgroundColor: theme.centerChannelBg
},
row: {
height: 40,
@@ -214,6 +162,12 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
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)
}
};
});

View File

@@ -6,56 +6,39 @@ import {createSelector} from 'reselect';
import {bindActionCreators} from 'redux';
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
import {autocompleteCustomEmojis} from 'mattermost-redux/actions/emojis';
import {Client4} from 'mattermost-redux/client';
import {addReactionToLatestPost} from 'app/actions/views/emoji';
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,
};
const emojis = getEmojisByName(state);
const list = emojis.length ? emojis : [];
const fuse = new Fuse(list, options);
return {
fuse,
emojis,
theme: getTheme(state),
serverVersion: state.entities.general.serverVersion || Client4.getServerVersion(),
theme: getTheme(state)
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
addReactionToLatestPost,
autocompleteCustomEmojis,
}, dispatch),
addReactionToLatestPost
}, dispatch)
};
}

View File

@@ -1,20 +1,89 @@
// 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,
value: PropTypes.string
};
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
}
})
}
});

View File

@@ -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);

View File

@@ -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,
},
};
});

View File

@@ -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,
},
};
});

View File

@@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity,
View,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
@@ -20,7 +20,7 @@ export default class SpecialMentionItem extends PureComponent {
id: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
values: PropTypes.object,
values: PropTypes.object
};
completeMention = () => {
@@ -34,7 +34,7 @@ export default class SpecialMentionItem extends PureComponent {
id,
completeHandle,
theme,
values,
values
} = this.props;
const style = getStyleFromTheme(theme);
@@ -71,30 +71,36 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
flexDirection: 'row',
alignItems: 'center',
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,
width: 20,
alignItems: 'center',
justifyContent: 'center',
justifyContent: 'center'
},
rowIcon: {
color: changeOpacity(theme.centerChannelColor, 0.7),
fontSize: 14,
fontSize: 14
},
rowUsername: {
fontSize: 13,
color: theme.centerChannelColor,
color: theme.centerChannelColor
},
rowFullname: {
color: theme.centerChannelColor,
flex: 1,
opacity: 0.6,
opacity: 0.6
},
textWrapper: {
flex: 1,
flexWrap: 'wrap',
paddingRight: 8,
},
paddingRight: 8
}
};
});

View File

@@ -9,14 +9,14 @@ import {
Text,
TouchableWithoutFeedback,
View,
ViewPropTypes,
ViewPropTypes
} from 'react-native';
export default class Badge extends PureComponent {
static defaultProps = {
extraPaddingHorizontal: 10,
minHeight: 0,
minWidth: 0,
minWidth: 0
};
static propTypes = {
@@ -26,14 +26,13 @@ export default class Badge extends PureComponent {
countStyle: Text.propTypes.style,
minHeight: PropTypes.number,
minWidth: PropTypes.number,
onPress: PropTypes.func,
onPress: PropTypes.func
};
constructor(props) {
super(props);
this.mounted = false;
this.layoutReady = false;
}
componentWillMount() {
@@ -42,7 +41,7 @@ export default class Badge extends PureComponent {
onMoveShouldSetPanResponder: () => true,
onStartShouldSetResponderCapture: () => true,
onMoveShouldSetResponderCapture: () => true,
onResponderMove: () => false,
onResponderMove: () => false
});
}
@@ -50,12 +49,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,25 +66,24 @@ export default class Badge extends PureComponent {
};
onLayout = (e) => {
if (!this.layoutReady) {
let width;
const height = Math.max(e.nativeEvent.layout.height, this.props.minHeight);
const borderRadius = height / 2;
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;
}
width = Math.max(width + 10, this.props.minWidth);
const borderRadius = width / 2;
this.setNativeProps({
style: {
width,
borderRadius,
opacity: 1,
},
});
this.layoutReady = true;
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);
this.setNativeProps({
style: {
width,
height,
borderRadius,
opacity: 1
}
});
};
renderText = () => {
@@ -139,23 +131,22 @@ export default class Badge extends PureComponent {
const styles = StyleSheet.create({
badge: {
backgroundColor: '#444',
borderRadius: 20,
height: 20,
top: 2,
padding: 12,
paddingTop: 3,
paddingBottom: 3,
backgroundColor: '#444',
borderRadius: 20,
position: 'absolute',
right: 30,
top: 2,
right: 30
},
wrapper: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
justifyContent: 'center'
},
text: {
fontSize: 14,
color: 'white',
},
color: 'white'
}
});

View File

@@ -11,19 +11,19 @@ import {GlobalStyles} from 'app/styles';
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
flexDirection: 'row'
},
loading: {
marginLeft: 3,
},
marginLeft: 3
}
});
export default class Button extends PureComponent {
static propTypes = {
children: PropTypes.node,
loading: PropTypes.bool,
onPress: PropTypes.func.isRequired,
onPress: PropTypes.func.isRequired
};
onPress = () => {

View File

@@ -9,28 +9,19 @@ import {
Keyboard,
Platform,
StyleSheet,
View,
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,11 +30,11 @@ 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,
setChannelLoading: PropTypes.func.isRequired,
setChannelLoading: PropTypes.func.isRequired
}).isRequired,
blurPostTextBox: PropTypes.func.isRequired,
children: PropTypes.node,
@@ -54,11 +45,11 @@ export default class ChannelDrawer extends Component {
intl: PropTypes.object.isRequired,
navigator: PropTypes.object,
teamsCount: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
closeHandle = null;
openHandle = null;
closeLeftHandle = null;
openLeftHandle = null;
swiperIndex = 1;
constructor(props) {
@@ -69,7 +60,7 @@ export default class ChannelDrawer extends Component {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.state = {
openDrawerOffset,
openDrawerOffset
};
}
@@ -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();
}
};
@@ -175,12 +168,12 @@ export default class ChannelDrawer extends Component {
mainOverlay: {
backgroundColor: this.props.theme.centerChannelBg,
elevation: 3,
opacity,
opacity
},
drawerOverlay: {
backgroundColor: ratio ? '#000' : '#FFF',
opacity: ratio ? (1 - ratio) / 2 : 1,
},
opacity: ratio ? (1 - ratio) / 2 : 1
}
};
};
@@ -202,7 +195,7 @@ export default class ChannelDrawer extends Component {
selectChannel = (channel, currentChannelId) => {
const {
actions,
actions
} = this.props;
const {
@@ -210,23 +203,21 @@ export default class ChannelDrawer extends Component {
markChannelAsRead,
setChannelLoading,
setChannelDisplayName,
markChannelAsViewed,
viewChannel
} = actions;
tracker.channelSwitch = Date.now();
setChannelLoading(channel.id !== currentChannelId);
setChannelDisplayName(channel.display_name);
this.closeChannelDrawer();
InteractionManager.runAfterInteractions(() => {
setChannelLoading(channel.id !== currentChannelId);
setChannelDisplayName(channel.display_name);
handleSelectChannel(channel.id);
requestAnimationFrame(() => {
// mark the channel as viewed after all the frame has flushed
markChannelAsRead(channel.id, currentChannelId);
if (channel.id !== currentChannelId) {
markChannelAsViewed(currentChannelId);
viewChannel(currentChannelId);
}
});
});
@@ -237,12 +228,12 @@ export default class ChannelDrawer extends Component {
actions,
currentTeamId,
currentUserId,
intl,
intl
} = this.props;
const {
joinChannel,
makeDirectChannel,
makeDirectChannel
} = actions;
const displayValue = {displayName: channel.display_name};
@@ -254,7 +245,7 @@ export default class ChannelDrawer extends Component {
if (result.error) {
const dmFailedMessage = {
id: 'mobile.open_dm.error',
defaultMessage: "We couldn't open a direct message with {displayName}. Please check your connection and try again.",
defaultMessage: "We couldn't open a direct message with {displayName}. Please check your connection and try again."
};
alertErrorWithFallback(intl, result.error, dmFailedMessage, displayValue);
}
@@ -264,7 +255,7 @@ export default class ChannelDrawer extends Component {
if (result.error) {
const joinFailedMessage = {
id: 'mobile.join_channel.error',
defaultMessage: "We couldn't join the channel {displayName}. Please check your connection and try again.",
defaultMessage: "We couldn't join the channel {displayName}. Please check your connection and try again."
};
alertErrorWithFallback(intl, result.error, joinFailedMessage, displayValue);
}
@@ -320,11 +311,11 @@ export default class ChannelDrawer extends Component {
const {
navigator,
teamsCount,
theme,
theme
} = this.props;
const {
openDrawerOffset,
openDrawerOffset
} = this.state;
const multipleTeams = teamsCount > 1;
@@ -371,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'
@@ -405,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}
@@ -420,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',
@@ -429,9 +411,9 @@ export default class ChannelDrawer extends Component {
shadowRadius: 12,
shadowOffset: {
width: -4,
height: 0,
},
},
height: 0
}
}
}}
>
{children}
@@ -443,6 +425,6 @@ export default class ChannelDrawer extends Component {
const style = StyleSheet.create({
swiperContent: {
flex: 1,
marginBottom: 10,
},
marginBottom: 10
}
});

View File

@@ -4,112 +4,55 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Animated,
Platform,
TouchableHighlight,
Text,
View,
View
} from 'react-native';
import {intlShape} from 'react-intl';
import Badge from 'app/components/badge';
import ChannelIcon from 'app/components/channel_icon';
import {preventDoubleTap} from 'app/utils/tap';
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,
isChannelMuted: PropTypes.bool,
isMyUser: PropTypes.bool,
isUnread: PropTypes.bool,
mentions: PropTypes.number.isRequired,
navigator: PropTypes.object,
onSelectChannel: PropTypes.func.isRequired,
shouldHideChannel: PropTypes.bool,
status: PropTypes.string,
teammateDeletedAt: PropTypes.number,
type: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
static contextTypes = {
intl: intlShape,
};
onPress = preventDoubleTap(() => {
onPress = wrapWithPreventDoubleTap(() => {
const {channelId, currentChannelId, displayName, fake, onSelectChannel, type} = this.props;
requestAnimationFrame(() => {
onSelectChannel({id: channelId, display_name: displayName, fake, type}, currentChannelId);
});
});
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,
isChannelMuted,
isMyUser,
isUnread,
mentions,
shouldHideChannel,
status,
teammateDeletedAt,
theme,
type,
type
} = this.props;
if (shouldHideChannel) {
return null;
}
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;
let extraItemStyle;
let extraTextStyle;
let extraBorder;
let mutedStyle;
if (isActive) {
extraItemStyle = style.itemActive;
@@ -136,10 +79,6 @@ export default class ChannelItem extends PureComponent {
);
}
if (isChannelMuted) {
mutedStyle = style.muted;
}
const icon = (
<ChannelIcon
isActive={isActive}
@@ -148,35 +87,31 @@ export default class ChannelItem extends PureComponent {
membersCount={displayName.split(',').length}
size={16}
status={status}
teammateDeletedAt={teammateDeletedAt}
theme={theme}
type={type}
/>
);
return (
<AnimatedView ref={this.setPreviewRef}>
<TouchableHighlight
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={this.onPress}
onLongPress={this.onPreview}
>
<View style={[style.container, mutedStyle]}>
{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>
);
}
}
@@ -186,37 +121,36 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
container: {
flex: 1,
flexDirection: 'row',
height: 44,
height: 44
},
borderActive: {
backgroundColor: theme.sidebarTextActiveBorder,
width: 5,
width: 5
},
item: {
alignItems: 'center',
height: 44,
flex: 1,
flexDirection: 'row',
paddingLeft: 16,
paddingLeft: 16
},
itemActive: {
backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.1),
paddingLeft: 11,
paddingLeft: 11
},
text: {
color: changeOpacity(theme.sidebarText, 0.4),
flex: 1,
fontSize: 14,
fontWeight: '600',
paddingRight: 40,
height: '100%',
flex: 1,
textAlignVertical: 'center',
lineHeight: 44,
lineHeight: 16,
paddingRight: 40
},
textActive: {
color: theme.sidebarTextActiveColor,
color: theme.sidebarTextActiveColor
},
textUnread: {
color: theme.sidebarUnreadText,
color: theme.sidebarUnreadText
},
badge: {
backgroundColor: theme.mentionBj,
@@ -225,14 +159,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
borderWidth: 1,
padding: 3,
position: 'relative',
right: 16,
right: 16
},
mention: {
color: theme.mentionColor,
fontSize: 10,
},
muted: {
opacity: 0.5,
},
fontSize: 10
}
};
});

View File

@@ -3,11 +3,8 @@
import {connect} from 'react-redux';
import {General} from 'mattermost-redux/constants';
import {getCurrentChannelId, makeGetChannel, getMyChannelMember, isChannelReadOnlyById} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentChannelId, makeGetChannel, getMyChannelMember} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users';
import {isChannelMuted} from 'mattermost-redux/utils/channel_utils';
import ChannelItem from './channel_item';
@@ -16,34 +13,19 @@ function makeMapStateToProps() {
return (state, ownProps) => {
const channel = ownProps.channel || getChannel(state, {id: ownProps.channelId});
const member = getMyChannelMember(state, ownProps.channelId);
const currentUserId = getCurrentUserId(state);
let isMyUser = false;
let teammateDeletedAt = 0;
if (channel.type === General.DM_CHANNEL && channel.teammate_id) {
isMyUser = channel.teammate_id === currentUserId;
const teammate = getUser(state, channel.teammate_id);
if (teammate && teammate.delete_at) {
teammateDeletedAt = teammate.delete_at;
}
let member;
if (ownProps.isUnread) {
member = getMyChannelMember(state, ownProps.channelId);
}
const isReadOnly = isChannelReadOnlyById(state, channel.id);
const shouldHideChannel = !ownProps.isSearchResult && !ownProps.isFavorite && isReadOnly;
return {
currentChannelId: getCurrentChannelId(state),
displayName: channel.display_name,
fake: channel.fake,
isChannelMuted: isChannelMuted(member),
isMyUser,
mentions: member ? member.mention_count : 0,
shouldHideChannel,
status: channel.status,
teammateDeletedAt,
theme: getTheme(state),
type: channel.type,
type: channel.type
};
};
}

View File

@@ -5,21 +5,21 @@ import React from 'react';
import PropTypes from 'prop-types';
import {
Platform,
View,
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,
@@ -29,15 +29,15 @@ class ChannelsList extends React.PureComponent {
onSearchStart: PropTypes.func.isRequired,
onSelectChannel: PropTypes.func.isRequired,
onShowTeams: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
constructor(props) {
super(props);
this.firstUnreadChannel = null;
this.state = {
searching: false,
term: '',
term: ''
};
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => {
@@ -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});
};
@@ -77,12 +101,13 @@ class ChannelsList extends React.PureComponent {
intl,
navigator,
onShowTeams,
theme,
theme
} = this.props;
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,17 +140,6 @@ class ChannelsList extends React.PureComponent {
);
}
const searchBarInput = {
backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.2),
color: theme.sidebarHeaderTextColor,
fontSize: 15,
...Platform.select({
android: {
marginBottom: -5,
},
}),
};
const title = (
<View style={styles.searchContainer}>
<SearchBar
@@ -120,8 +147,13 @@ class ChannelsList extends React.PureComponent {
placeholder={intl.formatMessage({id: 'mobile.channel_drawer.search', defaultMessage: 'Jump to...'})}
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={34}
inputStyle={searchBarInput}
inputHeight={33}
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)}
@@ -149,6 +181,7 @@ class ChannelsList extends React.PureComponent {
/>
</View>
{title}
{settings}
</View>
</View>
{list}
@@ -161,10 +194,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
backgroundColor: theme.sidebarBg,
flex: 1,
flex: 1
},
statusBar: {
backgroundColor: theme.sidebarHeaderBg,
...Platform.select({
ios: {
paddingTop: 20
}
})
},
headerContainer: {
alignItems: 'center',
@@ -175,30 +213,50 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
borderBottomColor: changeOpacity(theme.sidebarHeaderTextColor, 0.10),
...Platform.select({
android: {
height: ANDROID_TOP_PORTRAIT,
height: 46
},
ios: {
height: 44,
},
}),
height: 44
}
})
},
header: {
color: theme.sidebarHeaderTextColor,
flex: 1,
fontSize: 17,
fontWeight: 'normal',
paddingLeft: 16,
paddingLeft: 16
},
switchContainer: {
position: 'relative',
top: -1,
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,
flexDirection: 'row',
height: 48,
marginLeft: 16,
marginLeft: 16
},
title: {
flex: 1,
@@ -207,40 +265,39 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
fontSize: 15,
fontWeight: '400',
letterSpacing: 0.8,
lineHeight: 18,
lineHeight: 18
},
searchContainer: {
flex: 1,
paddingRight: 10,
...Platform.select({
android: {
marginBottom: 1,
marginBottom: 1
},
ios: {
marginBottom: 3,
},
}),
marginBottom: 3
}
})
},
divider: {
backgroundColor: changeOpacity(theme.sidebarText, 0.1),
height: 1,
height: 1
},
actionContainer: {
alignItems: 'center',
height: 48,
justifyContent: 'center',
width: 50,
width: 50
},
action: {
color: theme.sidebarText,
fontSize: 20,
fontWeight: '500',
lineHeight: 18,
lineHeight: 18
},
above: {
backgroundColor: theme.mentionBj,
top: 9,
},
top: 9
}
};
});

View File

@@ -8,7 +8,7 @@ import {
FlatList,
Text,
TouchableHighlight,
View,
View
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
@@ -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 = {
@@ -30,7 +27,7 @@ class FilteredList extends Component {
getProfilesInTeam: PropTypes.func.isRequired,
makeGroupMessageVisibleIfNecessary: PropTypes.func.isRequired,
searchChannels: PropTypes.func.isRequired,
searchProfiles: PropTypes.func.isRequired,
searchProfiles: PropTypes.func.isRequired
}).isRequired,
channels: PropTypes.object.isRequired,
currentTeam: PropTypes.object.isRequired,
@@ -49,18 +46,19 @@ class FilteredList extends Component {
statuses: PropTypes.object,
styles: PropTypes.object.isRequired,
term: PropTypes.string,
theme: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
static defaultProps = {
currentTeam: {},
currentChannel: {},
pastDirectMessages: [],
pastDirectMessages: []
};
constructor(props) {
super(props);
this.state = {
dataSource: this.buildData(props),
dataSource: this.buildData(props)
};
}
@@ -113,7 +111,6 @@ class FilteredList extends Component {
ref={channel.id}
channelId={channel.id}
channel={channel}
isSearchResult={true}
isUnread={false}
mentions={0}
onSelectChannel={this.onSelectChannel}
@@ -146,28 +143,28 @@ class FilteredList extends Component {
unreads: {
builder: this.buildUnreadChannelsForSearch,
id: 'mobile.channel_list.unreads',
defaultMessage: 'UNREADS',
defaultMessage: 'UNREADS'
},
channels: {
builder: this.buildChannelsForSearch,
id: 'mobile.channel_list.channels',
defaultMessage: 'CHANNELS',
id: 'sidebar.channels',
defaultMessage: 'CHANNELS'
},
dms: {
builder: this.buildCurrentDMSForSearch,
id: 'sidebar.direct',
defaultMessage: 'DIRECT MESSAGES',
defaultMessage: 'DIRECT MESSAGES'
},
members: {
builder: this.buildMembersForSearch,
id: 'mobile.channel_list.members',
defaultMessage: 'MEMBERS',
defaultMessage: 'MEMBERS'
},
nonmembers: {
builder: this.buildOtherMembersForSearch,
id: 'mobile.channel_list.not_member',
defaultMessage: 'NOT A MEMBER',
},
defaultMessage: 'NOT A MEMBER'
}
});
buildUnreadChannelsForSearch = (props, term) => {
@@ -212,14 +209,14 @@ class FilteredList extends Component {
type: General.DM_CHANNEL,
fake: true,
nickname: u.nickname,
fullname: `${u.first_name} ${u.last_name}`,
fullname: `${u.first_name} ${u.last_name}`
};
});
groupChannels = groupChannels.map((channel) => {
return {
...channel,
...groupChannelMemberDetails[channel.id],
...groupChannelMemberDetails[channel.id]
};
});
@@ -253,7 +250,7 @@ class FilteredList extends Component {
type: General.DM_CHANNEL,
fake: true,
nickname: u.nickname,
fullname: `${u.first_name} ${u.last_name}`,
fullname: `${u.first_name} ${u.last_name}`
};
});
@@ -266,7 +263,7 @@ class FilteredList extends Component {
const {
favoriteChannels,
publicChannels,
privateChannels,
privateChannels
} = props.channels;
const favorites = favoriteChannels.filter((c) => {
@@ -283,7 +280,7 @@ class FilteredList extends Component {
const notMemberOf = otherChannels.map((o) => {
return {
...o,
fake: true,
fake: true
};
});
@@ -369,7 +366,7 @@ class FilteredList extends Component {
</View>
{bottomDivider && this.renderDivider(styles, 16)}
</View>
),
)
};
};
@@ -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>
);

View File

@@ -13,10 +13,9 @@ import {
getChannelsWithUnreadSection,
getCurrentChannel,
getGroupChannels,
getOtherChannels,
getOtherChannels
} from 'mattermost-redux/selectors/entities/channels';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUserId, getProfilesInCurrentTeam, getUsers, getUserIdsInChannels, getUserStatuses} from 'mattermost-redux/selectors/entities/users';
import {getDirectShowPreferences, getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
@@ -73,14 +72,14 @@ function getGroupDetails(currentUserId, userIdsInChannels, profiles, groupChanne
email: [],
fullname: [],
nickname: [],
username: [],
username: []
});
groupMemberDetails[channel.id] = {
email: members.email.join(','),
fullname: members.fullname.join(','),
nickname: members.nickname.join(','),
username: members.username.join(','),
username: members.username.join(',')
};
return groupMemberDetails;
@@ -95,7 +94,7 @@ const getGroupChannelMemberDetails = createSelector(
getGroupDetails
);
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const {currentUserId} = state.entities.users;
const profiles = getUsers(state);
@@ -110,7 +109,6 @@ function mapStateToProps(state) {
return {
channels: getChannelsWithUnreadSection(state),
currentChannel: getCurrentChannel(state),
currentTeam: getCurrentTeam(state),
currentUserId,
otherChannels: getOtherChannels(state),
groupChannelMemberDetails: getGroupChannelMemberDetails(state),
@@ -122,6 +120,7 @@ function mapStateToProps(state) {
pastDirectMessages: pastDirectMessages(state),
restrictDms,
theme: getTheme(state),
...ownProps
};
}
@@ -131,8 +130,8 @@ function mapDispatchToProps(dispatch) {
getProfilesInTeam,
makeGroupMessageVisibleIfNecessary,
searchChannels,
searchProfiles,
}, dispatch),
searchProfiles
}, dispatch)
};
}

View File

@@ -9,7 +9,7 @@ import ChannelsList from './channels_list';
function mapStateToProps(state) {
return {
theme: getTheme(state),
theme: getTheme(state)
};
}

View File

@@ -9,13 +9,12 @@ import {
getSortedFavoriteChannelIds,
getSortedPublicChannelIds,
getSortedPrivateChannelIds,
getSortedDirectChannelIds,
getSortedDirectChannelIds
} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getTheme, getFavoritesPreferences} from 'mattermost-redux/selectors/entities/preferences';
import {showCreateOption} from 'mattermost-redux/utils/channel_utils';
import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils';
import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
import List from './list';
@@ -24,22 +23,18 @@ function mapStateToProps(state) {
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
const unreadChannelIds = getSortedUnreadChannelIds(state);
const favoriteChannelIds = getSortedFavoriteChannelIds(state);
const publicChannelIds = getSortedPublicChannelIds(state);
const privateChannelIds = getSortedPrivateChannelIds(state);
const directChannelIds = getSortedDirectChannelIds(state);
const currentTeamId = getCurrentTeamId(state);
const publicChannelIds = getSortedPublicChannelIds(state);
const isAdmin = checkIsAdmin(roles);
const isSystemAdmin = checkIsSystemAdmin(roles);
return {
canCreatePrivateChannels: showCreateOption(state, config, license, currentTeamId, General.PRIVATE_CHANNEL, isAdmin, isSystemAdmin),
canCreatePrivateChannels: showCreateOption(config, license, General.PRIVATE_CHANNEL, isAdmin(roles), isSystemAdmin(roles)),
unreadChannelIds,
favoriteChannelIds,
publicChannelIds,
privateChannelIds,
directChannelIds,
theme: getTheme(state),
theme: getTheme(state)
};
}

View File

@@ -8,9 +8,9 @@ import {
SectionList,
Text,
TouchableHighlight,
View,
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,31 +18,22 @@ 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 {preventDoubleTap} from 'app/utils/tap';
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,
privateChannelIds: PropTypes.array.isRequired,
styles: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
unreadChannelIds: PropTypes.array.isRequired,
};
static contextTypes = {
intl: intlShape,
unreadChannelIds: PropTypes.array.isRequired
};
constructor(props) {
@@ -51,7 +42,7 @@ export default class List extends PureComponent {
this.state = {
sections: this.buildSections(props),
showIndicator: false,
width: 0,
width: 0
};
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => {
@@ -66,7 +57,7 @@ export default class List extends PureComponent {
favoriteChannelIds,
publicChannelIds,
privateChannelIds,
unreadChannelIds,
unreadChannelIds
} = this.props;
if (nextProps.canCreatePrivateChannels !== canCreatePrivateChannels ||
@@ -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
});
}
}
@@ -93,7 +84,7 @@ export default class List extends PureComponent {
favoriteChannelIds,
publicChannelIds,
privateChannelIds,
unreadChannelIds,
unreadChannelIds
} = props;
const sections = [];
@@ -104,7 +95,7 @@ export default class List extends PureComponent {
data: unreadChannelIds,
renderItem: this.renderUnreadItem,
topSeparator: false,
bottomSeparator: true,
bottomSeparator: true
});
}
@@ -114,7 +105,7 @@ export default class List extends PureComponent {
defaultMessage: 'FAVORITES',
data: favoriteChannelIds,
topSeparator: unreadChannelIds.length > 0,
bottomSeparator: true,
bottomSeparator: true
});
}
@@ -124,7 +115,7 @@ export default class List extends PureComponent {
defaultMessage: 'PUBLIC CHANNELS',
data: publicChannelIds,
topSeparator: favoriteChannelIds.length > 0 || unreadChannelIds.length > 0,
bottomSeparator: publicChannelIds.length > 0,
bottomSeparator: publicChannelIds.length > 0
});
sections.push({
@@ -133,7 +124,7 @@ export default class List extends PureComponent {
defaultMessage: 'PRIVATE CHANNELS',
data: privateChannelIds,
topSeparator: true,
bottomSeparator: privateChannelIds.length > 0,
bottomSeparator: privateChannelIds.length > 0
});
sections.push({
@@ -142,15 +133,14 @@ export default class List extends PureComponent {
defaultMessage: 'DIRECT MESSAGES',
data: directChannelIds,
topSeparator: true,
bottomSeparator: directChannelIds.length > 0,
bottomSeparator: directChannelIds.length > 0
});
return sections;
};
goToCreatePrivateChannel = preventDoubleTap(() => {
const {navigator, theme} = this.props;
const {intl} = this.context;
goToCreatePrivateChannel = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
screen: 'CreateChannel',
@@ -162,18 +152,17 @@ export default class List extends PureComponent {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
screenBackgroundColor: theme.centerChannelBg
},
passProps: {
channelType: General.PRIVATE_CHANNEL,
closeButton: this.closeButton,
},
closeButton: this.closeButton
}
});
});
goToDirectMessages = preventDoubleTap(() => {
const {navigator, theme} = this.props;
const {intl} = this.context;
goToDirectMessages = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
screen: 'MoreDirectMessages',
@@ -185,20 +174,19 @@ export default class List extends PureComponent {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
screenBackgroundColor: theme.centerChannelBg
},
navigatorButtons: {
leftButtons: [{
id: 'close-dms',
icon: this.closeButton,
}],
},
icon: this.closeButton
}]
}
});
});
goToMoreChannels = preventDoubleTap(() => {
const {navigator, theme} = this.props;
const {intl} = this.context;
goToMoreChannels = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
screen: 'MoreChannels',
@@ -210,11 +198,11 @@ export default class List extends PureComponent {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
screenBackgroundColor: theme.centerChannelBg
},
passProps: {
closeButton: this.closeButton,
},
closeButton: this.closeButton
}
});
});
@@ -257,8 +245,6 @@ export default class List extends PureComponent {
return (
<ChannelItem
channelId={item}
isFavorite={this.props.favoriteChannelIds.includes(item)}
navigator={this.props.navigator}
onSelectChannel={this.onSelectChannel}
/>
);
@@ -269,21 +255,19 @@ 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,
defaultMessage,
id,
topSeparator,
topSeparator
} = section;
return (
@@ -302,10 +286,10 @@ 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,
animated: true
});
}
};
@@ -347,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}
@@ -359,3 +346,5 @@ export default class List extends PureComponent {
);
}
}
export default injectIntl(List);

View File

@@ -13,9 +13,10 @@ function mapStateToProps(state) {
return {
currentTeamId: team.id,
displayName: team.display_name,
mentionCount: getChannelDrawerBadgeCount(state),
teamsCount: getMyTeamsCount(state),
theme: getTheme(state),
theme: getTheme(state)
};
}

View File

@@ -4,38 +4,39 @@
import PropTypes from 'prop-types';
import React from 'react';
import {
Text,
TouchableHighlight,
View,
View
} from 'react-native';
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
import Badge from 'app/components/badge';
import {preventDoubleTap} from 'app/utils/tap';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import TeamIcon from 'app/components/team_icon';
export default class SwitchTeamsButton extends React.PureComponent {
static propTypes = {
currentTeamId: PropTypes.string,
displayName: PropTypes.string,
searching: PropTypes.bool.isRequired,
onShowTeams: PropTypes.func.isRequired,
mentionCount: PropTypes.number.isRequired,
teamsCount: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
showTeams = preventDoubleTap(() => {
showTeams = wrapWithPreventDoubleTap(() => {
this.props.onShowTeams();
});
render() {
const {
currentTeamId,
displayName,
mentionCount,
searching,
teamsCount,
theme,
theme
} = this.props;
if (!currentTeamId) {
@@ -68,14 +69,12 @@ export default class SwitchTeamsButton extends React.PureComponent {
<AwesomeIcon
name='chevron-left'
size={12}
style={styles.switcherArrow}
color={theme.sidebarHeaderBg}
/>
<View style={styles.switcherDivider}/>
<TeamIcon
teamId={currentTeamId}
styleContainer={styles.teamIconContainer}
styleText={styles.teamIconText}
/>
<Text style={styles.switcherTeam}>
{displayName.substr(0, 2).toUpperCase()}
</Text>
</View>
</TouchableHighlight>
{badge}
@@ -87,33 +86,26 @@ export default class SwitchTeamsButton extends React.PureComponent {
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
switcherContainer: {
backgroundColor: theme.sidebarHeaderTextColor,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
height: 32,
backgroundColor: theme.sidebarHeaderTextColor,
borderRadius: 2,
flexDirection: 'row',
height: 32,
justifyContent: 'center',
marginLeft: 6,
marginRight: 6,
paddingHorizontal: 3,
},
switcherArrow: {
color: theme.sidebarHeaderBg,
marginRight: 3,
marginRight: 5,
paddingHorizontal: 6
},
switcherDivider: {
backgroundColor: theme.sidebarHeaderBg,
height: 15,
marginHorizontal: 6,
width: 1,
width: 1
},
teamIconContainer: {
width: 26,
height: 26,
marginLeft: 3,
},
teamIconText: {
fontSize: 14,
switcherTeam: {
color: theme.sidebarHeaderBg,
fontFamily: 'OpenSans',
fontSize: 14
},
badge: {
backgroundColor: theme.mentionBj,
@@ -124,11 +116,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
padding: 3,
position: 'absolute',
left: -5,
top: -5,
top: -5
},
mention: {
color: theme.mentionColor,
fontSize: 10,
},
fontSize: 10
}
};
});

View File

@@ -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'}]
}
});

View File

@@ -6,23 +6,24 @@ import PropTypes from 'prop-types';
import {
TouchableWithoutFeedback,
View,
ViewPropTypes,
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,
style: ViewPropTypes.style,
onPress: PropTypes.func,
theme: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
static defaultProps = {
onPress: () => true,
onPress: () => true
};
render() {
@@ -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>
@@ -65,7 +65,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
position: 'absolute',
borderRadius: 15,
marginHorizontal: 15,
height: 25,
height: 25
},
indicatorText: {
backgroundColor: 'transparent',
@@ -74,11 +74,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
paddingVertical: 2,
paddingHorizontal: 4,
textAlign: 'center',
textAlignVertical: 'center',
},
arrow: {
position: 'relative',
bottom: -1,
},
textAlignVertical: 'center'
}
};
});

View File

@@ -16,12 +16,12 @@ export default class DrawerSwiper extends Component {
onPageSelected: PropTypes.func,
openDrawerOffset: PropTypes.number,
showTeams: PropTypes.bool.isRequired,
theme: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
static defaultProps = {
onPageSelected: () => true,
openDrawerOffset: 0,
openDrawerOffset: 0
};
shouldComponentUpdate(nextProps) {
@@ -64,7 +64,7 @@ export default class DrawerSwiper extends Component {
deviceWidth,
openDrawerOffset,
showTeams,
theme,
theme
} = this.props;
const initialPage = React.Children.count(children) - 1;
@@ -92,6 +92,6 @@ export default class DrawerSwiper extends Component {
const style = StyleSheet.create({
pagination: {
bottom: 0,
position: 'absolute',
},
position: 'absolute'
}
});

View File

@@ -12,7 +12,7 @@ import DraweSwiper from './drawer_swiper';
function mapStateToProps(state) {
return {
...getDimensions(state),
theme: getTheme(state),
theme: getTheme(state)
};
}

View File

@@ -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';
@@ -24,7 +24,7 @@ function mapStateToProps(state) {
isLandscape: isLandscape(state),
isTablet: isTablet(state),
teamsCount: getMyTeamsCount(state),
theme: getTheme(state),
theme: getTheme(state)
};
}
@@ -34,13 +34,13 @@ function mapDispatchToProps(dispatch) {
getTeams,
handleSelectChannel,
joinChannel,
markChannelAsViewed,
viewChannel,
makeDirectChannel,
markChannelAsRead,
setChannelDisplayName,
setChannelLoading,
}, dispatch),
setChannelLoading
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps, null, {withRef: true})(ChannelDrawer);
export default connect(mapStateToProps, mapDispatchToProps)(ChannelDrawer);

View File

@@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getCurrentTeamId, getMySortedTeamIds} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentTeamId, getJoinableTeamIds, getMySortedTeamIds} from 'mattermost-redux/selectors/entities/teams';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {handleTeamChange} from 'app/actions/views/select_team';
@@ -18,18 +18,19 @@ function mapStateToProps(state) {
const locale = getCurrentLocale(state);
return {
canJoinOtherTeams: getJoinableTeamIds(state).length > 0,
currentTeamId: getCurrentTeamId(state),
currentUrl: removeProtocol(getCurrentUrl(state)),
teamIds: getMySortedTeamIds(state, locale),
theme: getTheme(state),
theme: getTheme(state)
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
handleTeamChange,
}, dispatch),
handleTeamChange
}, dispatch)
};
}

View File

@@ -6,40 +6,32 @@ import PropTypes from 'prop-types';
import {
FlatList,
Platform,
StatusBar,
Text,
TouchableHighlight,
View,
View
} from 'react-native';
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 {preventDoubleTap} from 'app/utils/tap';
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({
handleTeamChange: PropTypes.func.isRequired,
handleTeamChange: PropTypes.func.isRequired
}).isRequired,
canJoinOtherTeams: PropTypes.bool.isRequired,
closeChannelDrawer: PropTypes.func.isRequired,
currentTeamId: PropTypes.string.isRequired,
currentUrl: PropTypes.string.isRequired,
intl: intlShape.isRequired,
navigator: PropTypes.object.isRequired,
teamIds: PropTypes.array.isRequired,
theme: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
constructor(props) {
@@ -51,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);
}
@@ -63,7 +53,7 @@ class TeamsList extends PureComponent {
});
};
goToSelectTeam = preventDoubleTap(() => {
goToSelectTeam = wrapWithPreventDoubleTap(() => {
const {currentUrl, intl, navigator, theme} = this.props;
navigator.showModal({
@@ -76,18 +66,18 @@ class TeamsList extends PureComponent {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
screenBackgroundColor: theme.centerChannelBg
},
navigatorButtons: {
leftButtons: [{
id: 'close-teams',
icon: this.closeButton,
}],
icon: this.closeButton
}]
},
passProps: {
currentUrl,
theme,
},
theme
}
});
});
@@ -105,22 +95,25 @@ class TeamsList extends PureComponent {
};
render() {
const {teamIds, theme} = this.props;
const {canJoinOtherTeams, teamIds, theme} = this.props;
const styles = getStyleSheet(theme);
const moreAction = (
<TouchableHighlight
style={styles.moreActionContainer}
onPress={this.goToSelectTeam}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<Text
style={styles.moreAction}
let moreAction;
if (canJoinOtherTeams) {
moreAction = (
<TouchableHighlight
style={styles.moreActionContainer}
onPress={this.goToSelectTeam}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
{'+'}
</Text>
</TouchableHighlight>
);
<Text
style={styles.moreAction}
>
{'+'}
</Text>
</TouchableHighlight>
);
}
return (
<View style={styles.container}>
@@ -138,7 +131,10 @@ class TeamsList extends PureComponent {
data={teamIds}
renderItem={this.renderItem}
keyExtractor={this.keyExtractor}
viewabilityConfig={VIEWABILITY_CONFIG}
viewabilityConfig={{
viewAreaCoveragePercentThreshold: 3,
waitForInteraction: false
}}
/>
</View>
);
@@ -149,10 +145,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
backgroundColor: theme.sidebarBg,
flex: 1,
flex: 1
},
statusBar: {
backgroundColor: theme.sidebarHeaderBg,
...Platform.select({
ios: {
paddingTop: 20
}
})
},
headerContainer: {
alignItems: 'center',
@@ -162,19 +163,19 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
borderBottomColor: changeOpacity(theme.sidebarHeaderTextColor, 0.10),
...Platform.select({
android: {
height: ANDROID_TOP_PORTRAIT,
height: 46
},
ios: {
height: 44,
},
}),
height: 44
}
})
},
header: {
color: theme.sidebarHeaderTextColor,
flex: 1,
fontSize: 17,
textAlign: 'center',
fontWeight: '600',
fontWeight: '600'
},
moreActionContainer: {
alignItems: 'center',
@@ -182,17 +183,17 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
width: 50,
...Platform.select({
android: {
height: ANDROID_TOP_PORTRAIT,
height: 46
},
ios: {
height: 44,
},
}),
height: 44
}
})
},
moreAction: {
color: theme.sidebarHeaderTextColor,
fontSize: 30,
},
fontSize: 30
}
};
});

View File

@@ -23,7 +23,7 @@ function makeMapStateToProps() {
displayName: team.display_name,
mentionCount: getMentionCount(state, ownProps.teamId),
name: team.name,
theme: getTheme(state),
theme: getTheme(state)
};
};
}

View File

@@ -6,14 +6,12 @@ import PropTypes from 'prop-types';
import {
Text,
TouchableHighlight,
View,
View
} from 'react-native';
import IonIcon from 'react-native-vector-icons/Ionicons';
import Badge from 'app/components/badge';
import TeamIcon from 'app/components/team_icon';
import {preventDoubleTap} from 'app/utils/tap';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class TeamsListItem extends React.PureComponent {
@@ -25,10 +23,10 @@ export default class TeamsListItem extends React.PureComponent {
name: PropTypes.string.isRequired,
selectTeam: PropTypes.func.isRequired,
teamId: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
selectTeam = preventDoubleTap(() => {
selectTeam = wrapWithPreventDoubleTap(() => {
this.props.selectTeam(this.props.teamId);
});
@@ -40,7 +38,7 @@ export default class TeamsListItem extends React.PureComponent {
mentionCount,
name,
teamId,
theme,
theme
} = this.props;
const styles = getStyleSheet(theme);
@@ -73,12 +71,11 @@ export default class TeamsListItem extends React.PureComponent {
onPress={this.selectTeam}
>
<View style={styles.teamContainer}>
<TeamIcon
teamId={teamId}
styleContainer={styles.teamIconContainer}
styleText={styles.teamIconText}
styleImage={styles.imageContainer}
/>
<View style={styles.teamIconContainer}>
<Text style={styles.teamIcon}>
{displayName.substr(0, 2).toUpperCase()}
</Text>
</View>
<View style={styles.teamNameContainer}>
<Text
numberOfLines={1}
@@ -107,43 +104,47 @@ export default class TeamsListItem extends React.PureComponent {
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
teamWrapper: {
marginTop: 20,
marginTop: 20
},
teamContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
marginHorizontal: 16,
marginHorizontal: 16
},
teamIconContainer: {
alignItems: 'center',
backgroundColor: theme.sidebarText,
borderRadius: 2,
height: 40,
justifyContent: 'center',
width: 40
},
teamIcon: {
color: theme.sidebarBg,
fontFamily: 'OpenSans',
fontSize: 18,
fontWeight: '600'
},
teamNameContainer: {
flex: 1,
flexDirection: 'column',
marginLeft: 10,
marginLeft: 10
},
teamName: {
color: theme.sidebarText,
fontSize: 18,
},
teamIconContainer: {
width: 40,
height: 40,
},
teamIconText: {
fontSize: 18,
fontSize: 18
},
teamUrl: {
color: changeOpacity(theme.sidebarText, 0.5),
fontSize: 12,
},
imageContainer: {
backgroundColor: theme.sidebarBg,
fontSize: 12
},
checkmarkContainer: {
alignItems: 'flex-end',
alignItems: 'flex-end'
},
checkmark: {
color: theme.sidebarText,
fontSize: 20,
fontSize: 20
},
badge: {
backgroundColor: theme.mentionBj,
@@ -154,11 +155,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
padding: 3,
position: 'absolute',
left: 45,
top: -7.5,
top: -7.5
},
mention: {
color: theme.mentionColor,
fontSize: 10,
},
fontSize: 10
}
};
});

View File

@@ -5,17 +5,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import {
Text,
View,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import {
ArchiveIcon,
AwayAvatar,
DndAvatar,
OfflineAvatar,
OnlineAvatar,
} from 'app/components/status_icons';
import {OnlineStatus, AwayStatus, OfflineStatus} from 'app/components/status_icons';
import {General} from 'mattermost-redux/constants';
@@ -29,30 +23,19 @@ export default class ChannelIcon extends React.PureComponent {
membersCount: PropTypes.number,
size: PropTypes.number,
status: PropTypes.string,
teammateDeletedAt: PropTypes.number,
theme: PropTypes.object.isRequired,
type: PropTypes.string.isRequired,
type: PropTypes.string.isRequired
};
static defaultProps = {
isActive: false,
isInfo: false,
isUnread: false,
size: 12,
size: 12
};
render() {
const {
isActive,
isUnread,
isInfo,
membersCount,
size,
status,
teammateDeletedAt,
theme,
type,
} = this.props;
const {isActive, isUnread, isInfo, membersCount, size, status, theme, type} = this.props;
const style = getStyleSheet(theme);
let activeIcon;
@@ -106,52 +89,31 @@ export default class ChannelIcon extends React.PureComponent {
</Text>
</View>
);
} else if (type === General.DM_CHANNEL && teammateDeletedAt) {
icon = (
<ArchiveIcon
width={size}
height={size}
color={offlineColor}
/>
);
} 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;
}
}
@@ -167,49 +129,49 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
marginRight: 12,
alignItems: 'center',
alignItems: 'center'
},
icon: {
color: changeOpacity(theme.sidebarText, 0.4),
color: changeOpacity(theme.sidebarText, 0.4)
},
iconActive: {
color: theme.sidebarTextActiveColor,
color: theme.sidebarTextActiveColor
},
iconUnread: {
color: theme.sidebarUnreadText,
color: theme.sidebarUnreadText
},
iconInfo: {
color: theme.centerChannelColor,
color: theme.centerChannelColor
},
groupBox: {
alignSelf: 'flex-start',
alignItems: 'center',
borderWidth: 1,
borderColor: changeOpacity(theme.sidebarText, 0.4),
justifyContent: 'center',
justifyContent: 'center'
},
groupBoxActive: {
borderColor: theme.sidebarTextActiveColor,
borderColor: theme.sidebarTextActiveColor
},
groupBoxUnread: {
borderColor: theme.sidebarUnreadText,
borderColor: theme.sidebarUnreadText
},
groupBoxInfo: {
borderColor: theme.centerChannelColor,
borderColor: theme.centerChannelColor
},
group: {
color: changeOpacity(theme.sidebarText, 0.4),
fontSize: 10,
fontWeight: '600',
fontWeight: '600'
},
groupActive: {
color: theme.sidebarTextActiveColor,
color: theme.sidebarTextActiveColor
},
groupUnread: {
color: theme.sidebarUnreadText,
color: theme.sidebarUnreadText
},
groupInfo: {
color: theme.centerChannelColor,
},
color: theme.centerChannelColor
}
};
});

View File

@@ -4,10 +4,9 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Platform,
Text,
TouchableOpacity,
View,
View
} from 'react-native';
import {getFullName} from 'mattermost-redux/utils/user_utils';
import {General} from 'mattermost-redux/constants';
@@ -26,32 +25,27 @@ class ChannelIntro extends PureComponent {
intl: intlShape.isRequired,
isLoadingPosts: PropTypes.bool,
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
goToUserProfile = (userId) => {
const {intl, navigator, theme} = this.props;
const options = {
navigator.push({
screen: 'UserProfile',
title: intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}),
animated: true,
backButtonTitle: '',
passProps: {
userId,
userId
},
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
};
if (Platform.OS === 'ios') {
navigator.push(options);
} else {
navigator.showModal(options);
}
screenBackgroundColor: theme.centerChannelBg
}
});
};
getDisplayName = (member) => {
@@ -75,7 +69,7 @@ class ChannelIntro extends PureComponent {
return currentChannelMembers.map((member) => (
<TouchableOpacity
key={member.id}
onPress={preventDoubleTap(() => this.goToUserProfile(member.id))}
onPress={() => preventDoubleTap(this.goToUserProfile, this, member.id)}
style={style.profile}
>
<ProfilePicture
@@ -83,6 +77,7 @@ class ChannelIntro extends PureComponent {
size={64}
statusBorderWidth={2}
statusSize={25}
statusIconSize={15}
/>
</TouchableOpacity>
));
@@ -95,7 +90,7 @@ class ChannelIntro extends PureComponent {
return currentChannelMembers.map((member, index) => (
<TouchableOpacity
key={member.id}
onPress={preventDoubleTap(() => this.goToUserProfile(member.id))}
onPress={() => preventDoubleTap(this.goToUserProfile, this, member.id)}
>
<Text style={style.displayName}>
{index === currentChannelMembers.length - 1 ? this.getDisplayName(member) : `${this.getDisplayName(member)}, `}
@@ -115,9 +110,9 @@ class ChannelIntro extends PureComponent {
<Text style={style.message}>
{intl.formatMessage({
id: 'mobile.intro_messages.DM',
defaultMessage: 'This is the start of your direct message history with {teammate}. Direct messages and files shared here are not shown to people outside this area.',
defaultMessage: 'This is the start of your direct message history with {teammate}. Direct messages and files shared here are not shown to people outside this area.'
}, {
teammate,
teammate
})}
</Text>
);
@@ -134,7 +129,7 @@ class ChannelIntro extends PureComponent {
<Text style={style.message}>
{intl.formatMessage({
id: 'intro_messages.group_message',
defaultMessage: 'This is the start of your group message history with these teammates. Messages and files shared here are not shown to people outside this area.',
defaultMessage: 'This is the start of your group message history with these teammates. Messages and files shared here are not shown to people outside this area.'
})}
</Text>
);
@@ -147,7 +142,7 @@ class ChannelIntro extends PureComponent {
const date = intl.formatDate(currentChannel.create_at, {
year: 'numeric',
month: 'long',
day: 'numeric',
day: 'numeric'
});
let mainMessageIntl;
@@ -162,9 +157,9 @@ class ChannelIntro extends PureComponent {
date,
type: intl.formatMessage({
id: 'intro_messages.channel',
defaultMessage: 'channel',
}),
},
defaultMessage: 'channel'
})
}
};
} else {
mainMessageIntl = {
@@ -175,20 +170,20 @@ class ChannelIntro extends PureComponent {
date,
type: intl.formatMessage({
id: 'intro_messages.channel',
defaultMessage: 'channel',
}),
},
defaultMessage: 'channel'
})
}
};
}
const mainMessage = intl.formatMessage({
id: mainMessageIntl.id,
defaultMessage: mainMessageIntl.defaultMessage,
defaultMessage: mainMessageIntl.defaultMessage
}, mainMessageIntl.values);
const anyMemberMessage = intl.formatMessage({
id: 'intro_messages.anyMember',
defaultMessage: ' Any member can join and read this channel.',
defaultMessage: ' Any member can join and read this channel.'
});
return (
@@ -196,9 +191,9 @@ class ChannelIntro extends PureComponent {
<Text style={style.channelTitle}>
{intl.formatMessage({
id: 'intro_messages.beginning',
defaultMessage: 'Beginning of {name}',
defaultMessage: 'Beginning of {name}'
}, {
name: currentChannel.display_name,
name: currentChannel.display_name
})}
</Text>
<Text style={style.message}>
@@ -216,25 +211,25 @@ class ChannelIntro extends PureComponent {
const date = intl.formatDate(currentChannel.create_at, {
year: 'numeric',
month: 'long',
day: 'numeric',
day: 'numeric'
});
const mainMessage = intl.formatMessage({
id: 'intro_messages.creator',
defaultMessage: 'This is the start of the {name} {type}, created by {creator} on {date}.',
defaultMessage: 'This is the start of the {name} {type}, created by {creator} on {date}.'
}, {
name: currentChannel.display_name,
creator: creatorName,
date,
type: intl.formatMessage({
id: 'intro_messages.group',
defaultMessage: 'private channel',
}),
defaultMessage: 'private channel'
})
});
const onlyInvitedMessage = intl.formatMessage({
id: 'intro_messages.onlyInvited',
defaultMessage: ' Only invited members can see this private channel.',
defaultMessage: ' Only invited members can see this private channel.'
});
return (
@@ -242,9 +237,9 @@ class ChannelIntro extends PureComponent {
<Text style={style.channelTitle}>
{intl.formatMessage({
id: 'intro_messages.beginning',
defaultMessage: 'Beginning of {name}',
defaultMessage: 'Beginning of {name}'
}, {
name: currentChannel.display_name,
name: currentChannel.display_name
})}
</Text>
<Text style={style.message}>
@@ -263,23 +258,23 @@ class ChannelIntro extends PureComponent {
<Text style={style.channelTitle}>
{intl.formatMessage({
id: 'intro_messages.beginning',
defaultMessage: 'Beginning of {name}',
defaultMessage: 'Beginning of {name}'
}, {
name: currentChannel.display_name,
name: currentChannel.display_name
})}
</Text>
<Text style={style.channelWelcome}>
{intl.formatMessage({
id: 'mobile.intro_messages.default_welcome',
defaultMessage: 'Welcome to {name}!',
defaultMessage: 'Welcome to {name}!'
}, {
name: currentChannel.display_name,
name: currentChannel.display_name
})}
</Text>
<Text style={style.message}>
{intl.formatMessage({
id: 'mobile.intro_messages.default_message',
defaultMessage: 'This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.',
defaultMessage: 'This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.'
})}
</Text>
</View>
@@ -354,42 +349,42 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
color: theme.centerChannelColor,
fontSize: 19,
fontWeight: '600',
marginBottom: 12,
marginBottom: 12
},
channelWelcome: {
color: theme.centerChannelColor,
marginBottom: 12,
marginBottom: 12
},
container: {
marginTop: 60,
marginHorizontal: 12,
marginBottom: 12,
marginBottom: 12
},
displayName: {
color: theme.centerChannelColor,
fontSize: 15,
fontWeight: '600',
fontWeight: '600'
},
message: {
color: changeOpacity(theme.centerChannelColor, 0.8),
fontSize: 15,
lineHeight: 22,
lineHeight: 22
},
namesContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
marginBottom: 12,
marginBottom: 12
},
profile: {
height: 67,
marginBottom: 12,
marginRight: 12,
marginRight: 12
},
profilesContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'flex-start',
},
justifyContent: 'flex-start'
}
};
});

View File

@@ -2,58 +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 {getChannelMembersForDm} from 'app/selectors/channel';
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 getChannelMembers = createSelector(
getCurrentUserId,
(state, channel) => getProfilesInChannel(state, channel.id),
(currentUserId, profilesInChannel) => {
const currentChannelMembers = profilesInChannel || [];
return currentChannelMembers.filter((m) => m.id !== 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);
}
return function mapStateToProps(state, ownProps) {
const currentChannel = getChannel(state, {id: ownProps.channelId}) || {};
const {status: getPostsRequestStatus} = state.requests.posts.getPosts;
const creator = currentChannel.creator_id === currentUser.id ? currentUser : state.entities.users.profiles[currentChannel.creator_id];
const postsInChannel = state.entities.posts.postsInChannel[currentChannel.id] || [];
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);

View File

@@ -15,22 +15,22 @@ export default class ChannelLink extends React.PureComponent {
channelsByName: PropTypes.object.isRequired,
actions: PropTypes.shape({
handleSelectChannel: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
}).isRequired,
setChannelDisplayName: PropTypes.func.isRequired
}).isRequired
};
constructor(props) {
super(props);
this.state = {
channel: this.getChannelFromChannelName(props),
channel: this.getChannelFromChannelName(props)
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.channelName !== this.props.channelName || nextProps.channelsByName !== this.props.channelsByName) {
this.setState({
channel: this.getChannelFromChannelName(nextProps),
channel: this.getChannelFromChannelName(nextProps)
});
}
}
@@ -63,7 +63,7 @@ export default class ChannelLink extends React.PureComponent {
const channel = this.state.channel;
if (!channel) {
return <Text style={this.props.textStyle}>{`~${this.props.channelName}`}</Text>;
return <Text style={this.props.textStyle}>{'~' + this.props.channelName}</Text>;
}
const suffix = this.props.channelName.substring(channel.name.length);
@@ -74,7 +74,7 @@ export default class ChannelLink extends React.PureComponent {
style={this.props.linkStyle}
onPress={this.handlePress}
>
{`~${channel.display_name}`}
{channel.display_name}
</Text>
{suffix}
</Text>

View File

@@ -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),
...ownProps
};
}
@@ -20,8 +21,8 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
handleSelectChannel,
setChannelDisplayName,
}, dispatch),
setChannelDisplayName
}, dispatch)
};
}

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