Compare commits

..

5 Commits

Author SHA1 Message Date
Harrison Healey
f5bd4bd752 Fixed MATTERMOST-MOBILE-ANDROID-87 (#1566) 2018-04-02 23:02:42 +03:00
Harrison Healey
868e4b8ad6 Fixed MATTERMOST-MOBILE-ANDROID-85 (#1565) 2018-04-02 23:02:12 +03:00
Elias Nahum
097244692c Fix iOS Share Extension Crash and Bump build and version numbers (1.7) (#1556)
* Fix iOS Extension crash

* Bump app version to 1.7.1 and build to 92
2018-03-29 16:17:19 +03:00
Harrison Healey
6a4b729bc1 MM-9940 Updated CommonMark to not use String.prototype.startsWith (#1542) 2018-03-28 22:50:36 +08:00
Harrison Healey
6d6735f016 Locked redux to 1.7 release branch (#1549) 2018-03-28 10:11:17 +03:00
652 changed files with 17131 additions and 48895 deletions

View File

@@ -12,27 +12,23 @@
"parser": "babel-eslint",
"plugins": [
"react",
"header"
"mocha"
],
"env": {
"browser": true,
"node": true,
"jquery": true,
"es6": true,
"jest": true
"es6": true
},
"globals": {
"after": true,
"afterAll": true,
"afterEach": true,
"before": true,
"beforeAll": true,
"beforeEach": true,
"describe": true,
"expect": true,
"it": true,
"jest": true,
"test": true
"describe": true,
"it": true,
"expect": true,
"before": true,
"beforeEach": true,
"after": true,
"afterEach": true
},
"rules": {
"array-bracket-spacing": [2, "never"],
@@ -47,7 +43,7 @@
"comma-dangle": [2, "always-multiline"],
"comma-spacing": [2, {"before": false, "after": true}],
"comma-style": [2, "last"],
"complexity": [0, 10],
"complexity": [1, 10],
"computed-property-spacing": [2, "never"],
"consistent-return": 2,
"consistent-this": [2, "self"],
@@ -60,9 +56,8 @@
"func-names": 2,
"func-style": [2, "declaration", { "allowArrowFunctions": true }],
"generator-star-spacing": [0, {"before": false, "after": true}],
"global-require": 0,
"global-require": 2,
"guard-for-in": 2,
"header/header": [2, "line", " Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.\n See LICENSE.txt for license information."],
"id-blacklist": 0,
"indent": [2, 4, {"SwitchCase": 0}],
"jsx-quotes": [2, "prefer-single"],
@@ -261,6 +256,7 @@
"vars-on-top": 0,
"wrap-iife": [2, "outside"],
"wrap-regex": 2,
"yoda": [2, "never", {"exceptRange": false, "onlyEquality": false}]
"yoda": [2, "never", {"exceptRange": false, "onlyEquality": false}],
"mocha/no-exclusive-tests": 2
}
}

7
.gitignore vendored
View File

@@ -20,7 +20,6 @@ build/
*.perspectivev3
!default.perspectivev3
xcuserdata
xcshareddata
*.xccheckout
*.moved-aside
DerivedData
@@ -29,7 +28,6 @@ DerivedData
*.apk
*.xcuserstate
project.xcworkspace
xcshareddata/
# Android/IntelliJ
#
@@ -72,7 +70,7 @@ tags
fastlane/report.xml
fastlane/Preview.html
*/fastlane/screenshots
fastlane/.env
# Sentry
android/sentry.properties
@@ -86,5 +84,4 @@ ios/sentry.properties
ios/Pods/
#editor-settings
.vscode
.vscode

View File

@@ -1,144 +1,5 @@
# Mattermost Mobile Apps Changelog
## 1.9.2 Release
- Release Date: June 27, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue where attached videos did not play for the poster
- Fixed an issue where "Jump to recent messages" from the permalink view did not direct the user to the bottom of the channel
- Fixed an issue where post comments did not identify which parent post they belonged to
- Fixed multiple issues with typing non-english characters in the post input box
- Fixed multiple issues causing random app crashes
- Fixed an issue where files from the Android Files app failed to upload
- Fixed an issue where the iOS share extension crashed when switching the team or channel
- Fixed an issue where files from the Microsoft app failed to upload
- Fixed an issue on Android devices where sharing files changed the file extension of the attachment
## 1.9.1 Release
- Release Date: June 23, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue with typing lag on Android devices
- Fixed an issue causing users to be logged out after upgrading to v1.9.0
- Fixed an issue where the ``in:`` and ``from:`` modifiers were not being added to the search field
## v1.9.0 Release
- Release Date: June 16, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Improved first load time on Android
- Significantly decreased first load time on Android devices from cold start.
#### iOS Files app support
- Added support for attaching files from the iOS Files app from within Mattermost.
#### Improved styling of push notification
- Improved the layout of message content, channel name and sender name in push notifications.
### Improvements
- Combined join/leave system messages.
- Added splash screen and channel loader improvements.
- Removed the desktop notification duration setting.
- Added cache team icon and set background to always be white if using a PNG file.
- Added whitelabel for icons and splash screen.
### Bug Fixes
- Fixed an issue where other user's display name did not render in combined system messages after joining the channel.
- Fixed an issue where posts incorrectly had "Commented on Someone's message" above them.
- Fixed an issue where deleting a post or its parent in permalink view left permalink view blank.
- Fixed an issue where "User is typing" message cut was off.
- Fixed an issue where `More New Messages Above` appeared at the top of new channel on joining.
- Fixed an issue where a user was not directed to Town Square when leaving a channel.
- Fixed an issue where long post were not collapsed on Android.
- Fixed an issue where a user's name was initially shown as "someone" when opening a direct message with the user.
- Fixed an issue where an error was received when trying to change the team or channel from the share extension.
- Fixed an issue where switching to a newly created channel from a push notification redirected a user to Town Square.
- Fixed an issue where a public channel made private did not disappear automatically from clients not part of the channel.
## v1.8.0 Release
- Release Date: April 27, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Image performance
- Images are now downloaded and stored locally for better performance
#### Flagged Posts and Recent Mentions
- Access all your flagged posts and recent mentions from the buttons in the sidebar
#### Muted Channels
- Added support for Muted Channels released with Mattermost server v4.9
### Improvements
- Date separators now appear between each posts in the search view
- Deactivated users are now filtered out of the channel members lists
- Direct Messages user list is now sorted by username first
- Added the option to Direct Message yourself from your user profile screen
- Improved performance on the post list
- Improved matching and display when searching for users in the Direct Message user list
### Bug Fixes
- Fixed an issue where emoji reactions could be added from the search view but did not appear
- Fixed an issue causing the app to crash when trying to share content from a custom keyboard
- Fixed an issue where team names were being sorted based on letter case
- Fixed an issue where username would not be inserted to the post draft when using experimental configuration settings
- Fixed an issue with nested bullet lists being cut off in the user interface
- Fixed an issue where private channels were listed in the public channels section of the channel autocomplete list
- Fixed an issue where a profile images could not be updated from the app
## 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

View File

@@ -1,6 +1,8 @@
# Code Contribution Guidelines
Thank you for your interest in contributing! Please see the [Mattermost Contribution Guide](http://docs.mattermost.com/developer/contribution-guide.html) which describes the process for making code contributions across Mattermost projects and [join our "Native Mobile Apps" community channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to ask questions from community members and the Mattermost core team.
Please see the [Mattermost Contribution Guide](http://docs.mattermost.com/developer/contribution-guide.html) which describes the process for making code contributions across Mattermost projects.
Note: Community work won't start until October 31, and no community pull requests will be accepted before then.
### Review Process for this Repo

View File

@@ -10,14 +10,14 @@ 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)
.npminstall: package.json
@if ! [ $(shell which npm 2> /dev/null) ]; then \
echo "npm is not installed https://npmjs.com"; \
.yarninstall: package.json
@if ! [ $(shell which 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 $@
@@ -43,23 +43,22 @@ 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 ## Installs dependencies and assets
check-style: .npminstall ## Runs eslint
check-style: .yarninstall ## Runs eslint
@echo Checking for style guide compliance
@npm run check
@yarn run check
clean: ## Cleans dependencies, previous builds and temp files
@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
@rm -rf ios/Pods
@rm -rf android/app/build
@echo Cleanup finished
post-install:
@@ -67,10 +66,7 @@ post-install:
@# Must remove the .babelrc for 0.42.0 to work correctly
@# Need to copy custom ImagePickerModule.java that implements correct permission checks for android
@rm node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java
@cp ./native_modules/ImagePickerModule.java node_modules/react-native-image-picker/android/src/main/java/com/imagepicker
@# Need to copy custom RNDocumentPicker.m that implements direct access to the document picker in iOS
@cp ./native_modules/RNDocumentPicker.m node_modules/react-native-document-picker/ios/RNDocumentPicker/RNDocumentPicker.m
@cp ./ImagePickerModule.java node_modules/react-native-image-picker/android/src/main/java/com/imagepicker
@rm -f node_modules/intl/.babelrc
@# Hack to get react-intl and its dependencies to work with react-native
@# Based off of https://github.com/este/este/blob/master/gulp/native-fix.js
@@ -78,20 +74,21 @@ post-install:
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-messageformat/package.json
@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
@sed -i'' -e "s|compile 'com.facebook.react:react-native:0.17.+'|compile 'com.facebook.react:react-native:+'|g" node_modules/react-native-bottom-sheet/android/build.gradle
@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/mattermost-redux && yarn run build
start: | pre-run ## Starts the React Native packager server
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start; \
node ./node_modules/react-native/local-cli/cli.js start; \
else \
echo React Native packager server already running; \
ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' > server.PID; \
fi
stop: ## Stops the React Native packager server
@@ -141,26 +138,18 @@ run: run-ios ## alias for run-ios
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; \
npm start & echo Running iOS app in development; \
if [ ! -z "${SIMULATOR}" ]; then \
react-native run-ios --simulator="${SIMULATOR}"; \
else \
react-native run-ios; \
fi; \
node ./node_modules/react-native/local-cli/cli.js start & echo Running iOS app in development; \
react-native run-ios --simulator="${SIMULATOR}"; \
wait; \
else \
echo Running iOS app in development; \
if [ ! -z "${SIMULATOR}" ]; then \
react-native run-ios --simulator="${SIMULATOR}"; \
else \
react-native run-ios; \
fi; \
react-native run-ios --simulator="${SIMULATOR}"; \
fi
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; \
npm start & echo Running Android app in development; \
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 \
@@ -177,18 +166,23 @@ run-android: | check-device-android pre-run prepare-android-build ## Runs the ap
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; \
npm start & echo; \
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; \
npm start & echo; \
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
@@ -197,21 +191,26 @@ build-android: | pre-run check-style prepare-android-build ## Creates an Android
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; \
npm start & echo; \
node ./node_modules/react-native/local-cli/cli.js start & echo; \
fi
@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; \
npm start & echo; \
node ./node_modules/react-native/local-cli/cli.js start & echo; \
fi
@echo "Building unsigned Android app"
@cd fastlane && NODE_ENV=production bundle exec fastlane android unsigned
@@ -219,7 +218,7 @@ unsigned-android: pre-run check-style prepare-android-build
@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
@yarn test
## Help documentation https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
help:

View File

@@ -973,6 +973,25 @@ Contact GitHub API Training Shop Blog About
---
## jsdom-global
jsdom-global will inject document, window and other DOM API into your Node.js environment. Useful for running, in Node.js, tests that are made for browsers.
* HOMEPAGE
* https://github.com/rstacruz/jsdom-global
* LICENSE
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## url-parse
This product contains 'url-parse.js', Small footprint URL parser that works seamlessly across Node.js and browser environments.
@@ -1038,6 +1057,88 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## NYC
Istanbul's state of the art command line interface
* HOMEPAGE
* https://github.com/istanbuljs/nyc
* LICENSE
ISC License
Copyright (c) 2015, Contributors
Permission to use, copy, modify, and/or distribute this software
for any purpose with or without fee is hereby granted, provided
that the above copyright notice and this permission notice
appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE
LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---
## Enzyme
JavaScript Testing utilities for React
* HOMEPAGE
* http://airbnb.io/enzyme/
* LICENSE
The MIT License (MIT)
Copyright (c) 2015 Airbnb, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## Chai
BDD / TDD assertion framework for node.js and the browser that can be paired with any testing framework.
* HOMEPAGE
* http://chaijs.com/
* LICENSE
MIT License
Copyright (c) 2016 Chai.js Assertion Library
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## TinyColor
This product contains 'tinycolor', a small, fast library for color manipulation and conversion in JavaScript. It allows many forms of input, while providing color conversions and other color utility functions
@@ -1072,6 +1173,40 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## react-native-search-box
This product contains a modified portion of 'react-native-search-box', a simple search box with animation for React Native.
* HOMEPAGE:
* https://github.com/crabstudio
* LICENSE:
The MIT License
Copyright (c) 2017 Crabstudio.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## react-native-exception-handler
This product contains 'react-native-exception-handler', A react native module that lets you to register a global error handler that can capture fatal/non fatal uncaught exceptions.
@@ -1428,6 +1563,40 @@ SOFTWARE.
---
## youtube-video-id
This product contains 'youtube-video-id', Extracts the YouTube video ID from a url or string.
* HOMEPAGE:
* https://github.com/remarkablemark/youtube-video-id
* LICENSE:
MIT License
Copyright (c) 2016 Menglin "Mark" Xu <mark@remarkablemark.org>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## react-native-video
This product contains 'react-native-video', A <Video> component for react-native.
@@ -1929,6 +2098,25 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
---
## react-native-orientation
This product contains a modified version of 'react-native-orientation'. Allows to listen to device orientation changes in React Native applications and programmatically set preferred orientation on a per screen basis.
* HOMEPAGE:
* https://github.com/yamill/react-native-orientation
* LICENSE:
ISC License
Copyright 2017 React Native Orientation
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---
## reselect
This project is a simple “selector” library for Redux.
@@ -1983,6 +2171,40 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
---
## socketcluster
SocketCluster is a fast, highly scalable HTTP + realtime server engine which lets you build multi-process realtime servers that make use of all CPU cores on a machine/instance.
* HOMEPAGE:
* https://github.com/SocketCluster/socketcluster
* LICENSE:
(The MIT License)
Copyright (c) 2013-2017 SocketCluster.io
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## commonmark
This product contains a modified version of 'commonmark'. CommonMark is a rationalized version of Markdown syntax, with a spec and BSD-licensed reference implementations in C and JavaScript.
@@ -2110,6 +2332,240 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE
---
## babel-polyfill
This project includes a custom regenerator runtime and core-js.
* HOMEPAGE:
* https://github.com/babel/babel/tree/master/packages/babel-polyfill
* LICENSE:
The MIT License (MIT)
Copyright (c) 2017 Brian Ng
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## react-native-media-controls
This product contains a modified version of 'react-native-media-controls' This project is a UI component to manipulate your media.
* HOMEPAGE:
* https://github.com/charliesbox/react-native-media-controls
* LICENSE:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---
## react-native-section-list-get-item-layout
This package provides a function that helps you construct the getItemLayout function for your SectionLists.
@@ -2202,99 +2658,3 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---
## mime-db
This is a database of all mime types.
* HOMEPAGE:
* https://github.com/jshttp/mime-db
* LICENSE:
The MIT License (MIT)
Copyright (c) 2014 Jonathan Ong me@jongleberry.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
## react-native-document-picker
Document Picker for React Native using Document Providers.
* HOMEPAGE:
* https://github.com/Elyx0/react-native-document-picker
* LICENSE:
MIT License
Copyright (c) 2016 Elyx0
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## react-native-image-gallery
Pure JavaScript image gallery component for iOS and Android.
* HOMEPAGE:
* https://github.com/archriss/react-native-image-gallery
* LICENSE:
---
## react-native-keychain
Keychain Access for React Native.
* HOMEPAGE:
* https://github.com/oblador/react-native-keychain
* LICENSE:
The MIT License (MIT)
Copyright (c) 2015 Joel Arvidsson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,17 +1,16 @@
# Mattermost Mobile
- **Supported Server versions:** 4.0+
- **Supported iOS versions:** 9.3+
- **Supported Android versions:** 5.0+
**Supported Server Versions:** 4.0+
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 14 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
**Supported iOS versions:** 9.3+
**Supported Android versions:** 5.0+
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](https://about.mattermost.com/default-enterprise-app-store).
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).
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.
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!
**Important:** If you self-compile the Mattermost Mobile apps you also need to self-compile and deploy your own [Mattermost Push Notification Service](https://github.com/mattermost/mattermost-push-proxy).
# How to Contribute
### Testing
@@ -21,7 +20,7 @@ To help with testing app updates before they're released, you can:
1. Sign up to be a beta tester
- [Android](https://play.google.com/apps/testing/com.mattermost.rnbeta)
- [iOS](https://mattermost-fastlane.herokuapp.com/)
2. Install the `Mattermost Beta` app. New updates in the Beta app are released each Monday. You will receive a notification when the new updates are available.
2. Install the `Mattermost Beta` app
3. File any bugs you find by filing a [GitHub issue](https://github.com/mattermost/mattermost-mobile/issues) with:
- Device information
- Repro steps
@@ -45,13 +44,13 @@ To help with testing app updates before they're released, you can:
App data is wiped from the device when a user logs out of the app. If the user is logged in when the account is deactivated, then within one minute the system logs the user out, and as a result all app data is wiped from the device.
### Can I connect to multiple Mattermost servers using the mobile apps?
### Can I connect to multiple Mattermost servers using the mobile apps?**
At the moment, we only support connecting to one server at a time. If you need to connect to multiple servers, please [upvote the feature request](https://mattermost.uservoice.com/forums/306457/suggestions/10975938) so we can track demand for it.
As a work around, you can install both the released "Mattermost" app and sign up to be a [tester](#testing) for the "Mattermost Beta" app so you can connect to two servers at once.
### Will there be second generation apps available for tablets?
### Will there be second generation apps available for tablets?**
We plan to add support for tablets in the future, but the timeline depends on how many people have a need for it. If you're looking for a tablet version, please help us out by [upvoting the feature request](https://mattermost.uservoice.com/forums/306457/suggestions/20082079)!

View File

@@ -73,9 +73,7 @@ import com.android.build.OutputFile
*/
project.ext.react = [
entryFile: "index.js",
bundleCommand: "unbundle",
bundleConfig: "packager/config.js"
entryFile: "index.js"
]
apply from: "../../node_modules/react-native/react.gradle"
@@ -83,8 +81,8 @@ apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
if (System.getenv("SENTRY_ENABLED") == "true") {
project.ext.sentryCli = [
logLevel: "error",
flavorAware: false
logLevel: "debug",
flavorAware: true
]
apply from: "../../node_modules/react-native-sentry/sentry.gradle"
@@ -106,15 +104,16 @@ def enableSeparateBuildPerCPUArchitecture = false
def enableProguardInReleaseBuilds = false
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdkVersion 25
buildToolsVersion "25.0.1"
defaultConfig {
applicationId "com.mattermost.rnbeta"
minSdkVersion 21
targetSdkVersion 23
versionCode 127
versionName "1.10.0"
versionCode 92
versionName "1.7.1"
multiDexEnabled true
ndk {
abiFilters "armeabi-v7a", "x86"
}
@@ -151,7 +150,6 @@ android {
unsigned.initWith(buildTypes.release)
unsigned {
signingConfig null
matchingFallbacks = ['debug', 'release']
}
}
// applicationVariants are e.g. debug, release
@@ -169,56 +167,39 @@ android {
}
}
repositories {
maven {
url 'https://maven.google.com'
}
}
configurations.all {
resolutionStrategy {
eachDependency { DependencyResolveDetails details ->
if (details.requested.name == 'android-jsc') {
details.useTarget group: details.requested.group, name: 'android-jsc-intl', version: 'r216113'
}
}
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.android.support:appcompat-v7:25.0.1"
implementation 'com.android.support:percent:25.3.1'
implementation "com.facebook.react:react-native:+" // From node_modules
implementation project(':react-native-document-picker')
implementation project(':react-native-keychain')
implementation project(':react-native-doc-viewer')
implementation project(':react-native-video')
implementation project(':react-native-navigation')
implementation project(':react-native-image-picker')
implementation project(':react-native-bottom-sheet')
implementation ('com.google.android.gms:play-services-gcm:9.4.0') {
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'
compile "com.facebook.react:react-native:+" // From node_modules
compile project(':react-native-navigation')
compile project(':react-native-image-picker')
compile project(':react-native-orientation')
compile project(':react-native-bottom-sheet')
compile ('com.google.android.gms:play-services-gcm:9.4.0') {
force = true;
}
implementation project(':react-native-device-info')
implementation project(':reactnativenotifications')
implementation project(':react-native-cookies')
implementation project(':react-native-linear-gradient')
implementation project(':react-native-vector-icons')
implementation project(':react-native-svg')
implementation project(':react-native-local-auth')
implementation project(':jail-monkey')
implementation project(':react-native-youtube')
implementation project(':react-native-sentry')
implementation project(':react-native-exception-handler')
implementation project(':react-native-fetch-blob')
compile project(':react-native-device-info')
compile project(':reactnativenotifications')
compile project(':react-native-cookies')
compile project(':react-native-linear-gradient')
compile project(':react-native-vector-icons')
compile project(':react-native-svg')
compile project(':react-native-local-auth')
compile project(':jail-monkey')
compile project(':react-native-youtube')
compile project(':react-native-sentry')
compile project(':react-native-exception-handler')
compile project(':react-native-fetch-blob')
// For animated GIF support
implementation 'com.facebook.fresco:animated-base-support:1.3.0'
compile 'com.facebook.fresco:animated-base-support:1.3.0'
// For WebP support, including animated WebP
implementation 'com.facebook.fresco:animated-gif:1.3.0'
implementation 'com.facebook.fresco:animated-webp:1.3.0'
implementation 'com.facebook.fresco:webpsupport:1.3.0'
compile 'com.facebook.fresco:animated-gif:1.3.0'
compile 'com.facebook.fresco:animated-webp:1.3.0'
compile 'com.facebook.fresco:webpsupport:1.3.0'
}
// Run this once to be able to run the application with BUCK

View File

@@ -57,6 +57,7 @@
android:name="com.reactnativenavigation.controllers.NavigationActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"/>
<activity
android:noHistory="false"
android:name="com.mattermost.share.ShareActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/app_name"

View File

@@ -1,93 +0,0 @@
package com.mattermost.react_native_interface;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.modules.storage.ReactDatabaseSupplier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
/**
* AsyncStorageHelper: Class that accesses React Native AsyncStorage Database synchronously
*/
public class AsyncStorageHelper {
// Static variables from: com.facebook.react.modules.storage.ReactDatabaseSupplier
static final String TABLE_CATALYST = "catalystLocalStorage";
static final String KEY_COLUMN = "key";
static final String VALUE_COLUMN = "value";
private static final int MAX_SQL_KEYS = 999;
Context mReactContext = null;
public AsyncStorageHelper(Context mReactContext) {
this.mReactContext = mReactContext;
}
public HashMap<String, String> multiGet(ReadableArray keys) {
HashMap<String, String> results = new HashMap<>(keys.size());
HashSet<String> keysRemaining = new HashSet<>();
String[] columns = {KEY_COLUMN, VALUE_COLUMN};
ReactDatabaseSupplier reactDatabaseSupplier = ReactDatabaseSupplier.getInstance(this.mReactContext);
for (int keyStart = 0; keyStart < keys.size(); keyStart += MAX_SQL_KEYS) {
int keyCount = Math.min(keys.size() - keyStart, MAX_SQL_KEYS);
Cursor cursor = reactDatabaseSupplier.get().query(
TABLE_CATALYST,
columns,
buildKeySelection(keyCount),
buildKeySelectionArgs(keys, keyStart, keyCount),
null,
null,
null);
keysRemaining.clear();
try {
if (cursor.getCount() != keys.size()) {
// some keys have not been found - insert them with null into the final array
for (int keyIndex = keyStart; keyIndex < keyStart + keyCount; keyIndex++) {
keysRemaining.add(keys.getString(keyIndex));
}
}
if (cursor.moveToFirst()) {
do {
results.put(cursor.getString(0), cursor.getString(1));
keysRemaining.remove(cursor.getString(0));
} while (cursor.moveToNext());
}
} catch (Exception e) {
return new HashMap<>(1);
} finally {
cursor.close();
}
for (String key : keysRemaining) {
results.put(key, null);
}
keysRemaining.clear();
}
return results;
}
private static String buildKeySelection(int selectionCount) {
String[] list = new String[selectionCount];
Arrays.fill(list, "?");
return KEY_COLUMN + " IN (" + TextUtils.join(", ", list) + ")";
}
private static String[] buildKeySelectionArgs(ReadableArray keys, int start, int count) {
String[] selectionArgs = new String[count];
for (int keyIndex = 0; keyIndex < count; keyIndex++) {
selectionArgs[keyIndex] = keys.getString(start + keyIndex);
}
return selectionArgs;
}
}

View File

@@ -1,68 +0,0 @@
package com.mattermost.react_native_interface;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableType;
import java.util.ArrayList;
/**
* KeysReadableArray: Helper class that abstracts boilerplate
*/
public class KeysReadableArray implements ReadableArray {
@Override
public int size() {
return 0;
}
@Override
public boolean isNull(int index) {
return false;
}
@Override
public boolean getBoolean(int index) {
return false;
}
@Override
public double getDouble(int index) {
return 0;
}
@Override
public int getInt(int index) {
return 0;
}
@Override
public String getString(int index) {
return null;
}
@Override
public ReadableArray getArray(int index) {
return null;
}
@Override
public ReadableMap getMap(int index) {
return null;
}
@Override
public Dynamic getDynamic(int index) {
return null;
}
@Override
public ReadableType getType(int index) {
return null;
}
@Override
public ArrayList<Object> toArrayList() {
return null;
}
}

View File

@@ -1,38 +0,0 @@
package com.mattermost.react_native_interface;
import com.facebook.react.bridge.Promise;
/**
* ResolvePromise: Helper class that abstracts boilerplate
*/
public class ResolvePromise implements Promise {
@Override
public void resolve(@javax.annotation.Nullable Object value) {
}
@Override
public void reject(String code, String message) {
}
@Override
public void reject(String code, Throwable e) {
}
@Override
public void reject(String code, String message, Throwable e) {
}
@Override
public void reject(String message) {
}
@Override
public void reject(Throwable reason) {
}
}

View File

@@ -143,16 +143,8 @@ public class CustomPushNotification extends PushNotification {
// First, get a builder initialized with defaults from the core class.
final Notification.Builder notification = new Notification.Builder(mContext);
Bundle bundle = mNotificationProps.asBundle();
String version = bundle.getString("version");
String title = null;
if (version != null && version.equals("v2")) {
title = bundle.getString("channel_name");
} else {
title = bundle.getString("title");
}
if (android.text.TextUtils.isEmpty(title)) {
String title = bundle.getString("title");
if (title == null) {
ApplicationInfo appInfo = mContext.getApplicationInfo();
title = mContext.getPackageManager().getApplicationLabel(appInfo).toString();
}
@@ -215,13 +207,7 @@ public class CustomPushNotification extends PushNotification {
.setStyle(new Notification.BigTextStyle()
.bigText(message));
} else {
String summaryTitle = null;
if (version != null && version.equals("v2")) {
summaryTitle = String.format("(%d) %s", numMessages, title);
} else {
summaryTitle = String.format("%s (%d)", title, numMessages);
}
String summaryTitle = String.format("%s (%d)", title, numMessages);
Notification.InboxStyle style = new Notification.InboxStyle();
List<Bundle> bundleArray = channelIdToNotification.get(channelId);
@@ -232,10 +218,6 @@ public class CustomPushNotification extends PushNotification {
list = new ArrayList<Bundle>();
}
if (version != null && version.equals("v2")) {
style.addLine(message);
}
for (Bundle data : list) {
String msg = data.getString("message");
if (msg != message) {
@@ -243,17 +225,10 @@ public class CustomPushNotification extends PushNotification {
}
}
if (version != null && version.equals("v2")) {
notification
.setContentTitle(summaryTitle)
.setContentText(message)
.setStyle(style);
} else {
style.setBigContentTitle(message)
.setSummaryText(String.format("+%d more", (numMessages - 1)));
notification.setStyle(style)
.setContentTitle(summaryTitle);
}
style.setBigContentTitle(message)
.setSummaryText(String.format("+%d more", (numMessages - 1)));
notification.setStyle(style)
.setContentTitle(summaryTitle);
}
// Let's add a delete intent when the notification is dismissed

View File

@@ -1,146 +0,0 @@
package com.mattermost.rnbeta;
import android.app.Application;
import android.support.annotation.Nullable;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import com.mattermost.react_native_interface.AsyncStorageHelper;
import com.mattermost.react_native_interface.KeysReadableArray;
import com.mattermost.react_native_interface.ResolvePromise;
import com.oblador.keychain.KeychainModule;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class InitializationModule extends ReactContextBaseJavaModule {
static final String TOOLBAR_BACKGROUND = "TOOLBAR_BACKGROUND";
static final String TOOLBAR_TEXT_COLOR = "TOOLBAR_TEXT_COLOR";
static final String APP_BACKGROUND = "APP_BACKGROUND";
private final Application mApplication;
public InitializationModule(Application application, ReactApplicationContext reactContext) {
super(reactContext);
mApplication = application;
}
@Override
public String getName() {
return "Initialization";
}
@Nullable
@Override
public Map<String, Object> getConstants() {
Map<String, Object> constants = new HashMap<>();
/**
* Package all native module variables in constants
* in order to avoid the native bridge
*
* KeyStore:
* credentialsExist
* deviceToken
* currentUserId
* token
* url
*
* AsyncStorage:
* toolbarBackground
* toolbarTextColor
* appBackground
*
* Miscellaneous:
* MattermostManaged.Config
* replyFromPushNotification
*/
MainApplication app = (MainApplication) mApplication;
final Boolean[] credentialsExist = {false};
final WritableMap[] credentials = {null};
final Object[] config = {null};
// Get KeyStore credentials
KeychainModule module = new KeychainModule(this.getReactApplicationContext());
module.getGenericPasswordForOptions(null, new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
if (value instanceof Boolean && !(Boolean)value) {
credentialsExist[0] = false;
return;
}
WritableMap map = (WritableMap) value;
if (map != null) {
credentialsExist[0] = true;
credentials[0] = map;
}
}
});
// Get managedConfig from MattermostManagedModule
MattermostManagedModule.getInstance().getConfig(new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
WritableNativeMap nativeMap = (WritableNativeMap) value;
config[0] = value;
}
});
// Get AsyncStorage key/values
final ArrayList<String> keys = new ArrayList<String>(5);
keys.add(TOOLBAR_BACKGROUND);
keys.add(TOOLBAR_TEXT_COLOR);
keys.add(APP_BACKGROUND);
KeysReadableArray asyncStorageKeys = new KeysReadableArray() {
@Override
public int size() {
return keys.size();
}
@Override
public String getString(int index) {
return keys.get(index);
}
};
AsyncStorageHelper asyncStorage = new AsyncStorageHelper(this.getReactApplicationContext());
HashMap<String, String> asyncStorageResults = asyncStorage.multiGet(asyncStorageKeys);
String toolbarBackground = asyncStorageResults.get(TOOLBAR_BACKGROUND);
String toolbarTextColor = asyncStorageResults.get(TOOLBAR_TEXT_COLOR);
String appBackground = asyncStorageResults.get(APP_BACKGROUND);
if (toolbarBackground != null
&& toolbarTextColor != null
&& appBackground != null) {
constants.put("themesExist", true);
constants.put("toolbarBackground", toolbarBackground);
constants.put("toolbarTextColor", toolbarTextColor);
constants.put("appBackground", appBackground);
} else {
constants.put("themesExist", false);
}
if (credentialsExist[0]) {
constants.put("credentialsExist", true);
constants.put("credentials", credentials[0]);
} else {
constants.put("credentialsExist", false);
}
constants.put("managedConfig", config[0]);
constants.put("replyFromPushNotification", app.replyFromPushNotification);
app.replyFromPushNotification = false;
return constants;
}
}

View File

@@ -1,36 +0,0 @@
package com.mattermost.rnbeta;
import android.app.Application;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class InitializationPackage implements ReactPackage {
private final Application mApplication;
public InitializationPackage(Application application) {
mApplication = application;
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new InitializationModule(mApplication, reactContext));
}
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

View File

@@ -1,13 +1,12 @@
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.reactnativedocumentpicker.ReactNativeDocumentPicker;
import com.oblador.keychain.KeychainPackage;
import com.facebook.react.ReactApplication;
import com.reactlibrary.RNReactNativeDocViewerPackage;
import com.brentvatne.react.ReactVideoPackage;
import com.horcrux.svg.SvgPackage;
@@ -17,8 +16,10 @@ import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerPackage;
import com.RNFetchBlob.RNFetchBlobPackage;
import com.gantix.JailMonkey.JailMonkeyPackage;
import io.tradle.react.LocalAuthPackage;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
import com.imagepicker.ImagePickerPackage;
@@ -27,6 +28,7 @@ import com.learnium.RNDeviceInfo.RNDeviceInfo;
import com.psykar.cookiemanager.CookieManagerPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import com.github.yamill.orientation.OrientationPackage;
import com.reactnativenavigation.NavigationApplication;
import com.wix.reactnativenotifications.RNNotificationsPackage;
import com.wix.reactnativenotifications.core.notification.INotificationsApplication;
@@ -40,8 +42,6 @@ import java.util.List;
public class MainApplication extends NavigationApplication implements INotificationsApplication {
public NotificationsLifecycleFacade notificationsLifecycleFacade;
public Boolean sharedExtensionIsOpened = false;
public Boolean replyFromPushNotification = false;
@Override
public boolean isDebug() {
@@ -61,6 +61,7 @@ public class MainApplication extends NavigationApplication implements INotificat
new VectorIconsPackage(),
new SvgPackage(),
new LinearGradientPackage(),
new OrientationPackage(),
new RNNotificationsPackage(this),
new LocalAuthPackage(),
new JailMonkeyPackage(),
@@ -71,10 +72,7 @@ public class MainApplication extends NavigationApplication implements INotificat
new ReactNativeYouTube(),
new ReactVideoPackage(),
new RNReactNativeDocViewerPackage(),
new ReactNativeDocumentPicker(),
new SharePackage(this),
new KeychainPackage(),
new InitializationPackage(this)
new SharePackage()
);
}

View File

@@ -32,8 +32,6 @@ public class NotificationReplyService extends HeadlessJsTaskService {
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
MainApplication app = (MainApplication) this.getApplication();
app.replyFromPushNotification = true;
Log.i("ReactNative", "Replying service");
return new HeadlessJsTaskConfig(
"notificationReplied",

View File

@@ -7,11 +7,8 @@ import android.os.Build;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.content.ContentUris;
import android.content.ContentResolver;
import android.os.Environment;
import android.webkit.MimeTypeMap;
import android.util.Log;
import android.text.TextUtils;
import android.os.ParcelFileDescriptor;
import java.io.*;
@@ -39,19 +36,10 @@ public class RealPathUtil {
// DownloadsProvider
final String id = DocumentsContract.getDocumentId(uri);
if (!TextUtils.isEmpty(id)) {
if (id.startsWith("raw:")) {
return id.replaceFirst("raw:", "");
}
try {
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
} catch (NumberFormatException e) {
Log.e("ReactNative", "DownloadsProvider unexpected uri " + uri.toString());
return null;
}
}
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
@@ -75,15 +63,19 @@ public class RealPathUtil {
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
if ("content".equalsIgnoreCase(uri.getScheme())) {
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
// MediaStore (and general)
if (isGooglePhotosUri(uri)) {
return uri.getLastPathSegment();
}
String path = getDataColumn(context, uri, null, null);
if (path != null) {
return path;
}
// Try save to tmp file, and return tmp file path
return getPathFromSavingTempFile(context, uri);
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
@@ -97,13 +89,7 @@ public class RealPathUtil {
File tmpFile;
try {
String fileName = uri.getLastPathSegment();
File cacheDir = new File(context.getCacheDir(), "mmShare");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
String mimeType = getMimeType(uri.getPath());
tmpFile = File.createTempFile("tmp", fileName, cacheDir);
tmpFile = File.createTempFile("tmp", fileName, context.getCacheDir());
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
@@ -186,27 +172,4 @@ public class RealPathUtil {
File file = new File(filePath);
return getMimeType(file);
}
public static String getMimeTypeFromUri(final Context context, final Uri uri) {
ContentResolver cR = context.getContentResolver();
return cR.getType(uri);
}
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,20 +1,13 @@
package com.mattermost.share;
import android.content.Intent;
import android.os.Bundle;
import com.facebook.react.ReactActivity;
import com.mattermost.rnbeta.MainApplication;
public class ShareActivity extends ReactActivity {
@Override
protected String getMainComponentName() {
return "MattermostShare";
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainApplication app = (MainApplication) this.getApplication();
app.sharedExtensionIsOpened = true;
}
}

View File

@@ -2,6 +2,7 @@ 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;
@@ -9,25 +10,23 @@ 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 com.mattermost.rnbeta.MainApplication;
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.Nullable;
import javax.annotation.Nonnull;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
@@ -38,13 +37,9 @@ 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");
private final MainApplication mApplication;
public ShareModule(MainApplication application, ReactApplicationContext reactContext) {
public ShareModule(ReactApplicationContext reactContext) {
super(reactContext);
mApplication = application;
}
private File tempFolder;
@Override
public String getName() {
@@ -63,18 +58,8 @@ public class ShareModule extends ReactContextBaseJavaModule {
}
}
@Nullable
@Override
public Map<String, Object> getConstants() {
HashMap<String, Object> constants = new HashMap<>(1);
constants.put("isOpened", mApplication.sharedExtensionIsOpened);
mApplication.sharedExtensionIsOpened = false;
return constants;
}
@ReactMethod
public void close(ReadableMap data) {
this.clear();
getCurrentActivity().finish();
if (data != null) {
@@ -93,8 +78,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
}
}
}
RealPathUtil.deleteTempFiles(this.tempFolder);
}
@ReactMethod
@@ -102,23 +85,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
promise.resolve(processIntent());
}
@ReactMethod
public void getFilePath(String filePath, Promise promise) {
Activity currentActivity = getCurrentActivity();
WritableMap map = Arguments.createMap();
if (currentActivity != null) {
Uri uri = Uri.parse(filePath);
String path = RealPathUtil.getRealPathFromURI(currentActivity, uri);
if (path != null) {
String text = "file://" + path;
map.putString("filePath", text);
}
}
promise.resolve(map);
}
public WritableArray processIntent() {
WritableMap map = Arguments.createMap();
WritableArray items = Arguments.createArray();
@@ -130,7 +96,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
Activity currentActivity = getCurrentActivity();
if (currentActivity != null) {
this.tempFolder = new File(currentActivity.getCacheDir(), "mmShare");
Intent intent = currentActivity.getIntent();
action = intent.getAction();
type = intent.getType();
@@ -147,13 +112,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
text = "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri);
map.putString("value", text);
if (type.equals("image/*")) {
type = "image/jpeg";
} else if (type.equals("video/*")) {
type = "video/mp4";
}
map.putString("type", type);
items.pushMap(map);
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
@@ -163,15 +121,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
map = Arguments.createMap();
text = "file://" + filePath;
map.putString("value", text);
type = RealPathUtil.getMimeTypeFromUri(currentActivity, uri);
if (type.equals("image/*")) {
type = "image/jpeg";
} else if (type.equals("video/*")) {
type = "video/mp4";
}
map.putString("type", type);
map.putString("type", RealPathUtil.getMimeType(filePath));
items.pushMap(map);
}
}

View File

@@ -5,22 +5,15 @@ import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.ReactPackage;
import com.mattermost.rnbeta.MainApplication;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class SharePackage implements ReactPackage {
MainApplication mApplication;
public SharePackage(MainApplication application) {
mApplication = application;
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new ShareModule(mApplication, reactContext));
return Arrays.<NativeModule>asList(new ShareModule(reactContext));
}
public List<Class<? extends JavaScriptModule>> createJSModules() {

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

@@ -2,11 +2,10 @@
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0'
classpath 'com.android.tools.build:gradle:2.2.+'
classpath 'com.google.gms:google-services:3.1.0'
// NOTE: Do not place your application dependencies here; they belong
@@ -16,31 +15,11 @@ buildscript {
allprojects {
repositories {
google()
mavenLocal()
jcenter()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/../node_modules/react-native/android"
}
maven {
// Local Maven repo containing AARs with JSC library built for Android
url "$rootDir/../node_modules/jsc-android/android"
}
}
}
ext {
compileSdkVersion = 26
buildToolsVersion = '26.0.2'
}
subprojects { subproject ->
afterEvaluate{
if((subproject.plugins.hasPlugin('android') || subproject.plugins.hasPlugin('android-library'))) {
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
}
}
}
}

View File

@@ -17,5 +17,4 @@
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.enableAapt2=false
android.useDeprecatedNdk=true

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

View File

@@ -1,8 +1,4 @@
rootProject.name = 'Mattermost'
include ':react-native-document-picker'
project(':react-native-document-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-document-picker/android')
include ':react-native-keychain'
project(':react-native-keychain').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keychain/android')
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'
@@ -37,5 +33,7 @@ project(':reactnativenotifications').projectDir = new File(rootProject.projectDi
include ':app'
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':react-native-orientation'
project(':react-native-orientation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-orientation/android')
include ':react-native-linear-gradient'
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')

View File

@@ -1,8 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {Dimensions} from 'react-native';
import {DeviceTypes} from 'app/constants';
export function calculateDeviceDimensions() {
const {height, width} = Dimensions.get('window');
return {
type: DeviceTypes.DEVICE_DIMENSIONS_CHANGED,
data: {
deviceHeight: height,
deviceWidth: width,
},
};
}
export function connection(isOnline) {
return async (dispatch, getState) => {
dispatch({
@@ -19,16 +32,6 @@ export function setStatusBarHeight(height = 20) {
};
}
export function setDeviceDimensions(height, width) {
return {
type: DeviceTypes.DEVICE_DIMENSIONS_CHANGED,
data: {
deviceHeight: height,
deviceWidth: width,
},
};
}
export function setDeviceOrientation(orientation) {
return {
type: DeviceTypes.DEVICE_ORIENTATION_CHANGED,
@@ -44,8 +47,8 @@ export function setDeviceAsTablet() {
}
export default {
calculateDeviceDimensions,
connection,
setDeviceDimensions,
setDeviceOrientation,
setDeviceAsTablet,
setStatusBarHeight,

View File

@@ -1,15 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {updateMe} from 'mattermost-redux/actions/users';
import {Preferences} from 'mattermost-redux/constants';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
export function handleUpdateUserNotifyProps(notifyProps) {
return async (dispatch, getState) => {
const state = getState();
const config = getConfig(state);
const config = state.entities.general.config;
const {currentUserId} = state.entities.users;
const {interval, user_id: userId, ...otherProps} = notifyProps;

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {batchActions} from 'redux-batched-actions';
@@ -28,11 +28,9 @@ import {
isDirectChannel,
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;
@@ -144,7 +142,8 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
if (channel) {
actions.push({
type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL,
data: {id: channel.id, user_id: members[i]},
data: {user_id: members[i]},
id: channel.id,
});
}
}
@@ -208,7 +207,7 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
export async function retryGetPostsAction(action, dispatch, getState, maxTries = MAX_POST_TRIES) {
for (let i = 0; i < maxTries; i++) {
const {data} = await dispatch(action);
const {data} = await action(dispatch, getState);
if (data) {
dispatch(setChannelRetryFailed(false));
@@ -244,7 +243,7 @@ export function loadThreadIfNecessary(rootId, channelId) {
}
export function selectInitialChannel(teamId) {
return (dispatch, getState) => {
return async (dispatch, getState) => {
const state = getState();
const {channels, myMembers} = state.entities.channels;
const {currentUserId} = state.entities.users;
@@ -266,14 +265,6 @@ export function selectInitialChannel(teamId) {
return;
}
dispatch(selectDefaultChannel(teamId));
};
}
export function selectDefaultChannel(teamId) {
return (dispatch, getState) => {
const channels = getState().entities.channels.channels;
const channel = Object.values(channels).find((c) => c.team_id === teamId && c.name === General.DEFAULT_CHANNEL);
let channelId;
if (channel) {
@@ -289,8 +280,8 @@ export function selectDefaultChannel(teamId) {
if (channelId) {
dispatch(setChannelDisplayName(''));
dispatch(handleSelectChannel(channelId));
dispatch(markChannelAsRead(channelId));
handleSelectChannel(channelId)(dispatch, getState);
markChannelAsRead(channelId)(dispatch, getState);
}
};
}
@@ -329,18 +320,58 @@ export function handlePostDraftChanged(channelId, draft) {
};
}
export function insertToDraft(value) {
return (dispatch, getState) => {
const state = getState();
const threadId = state.entities.posts.selectedPostId;
const insertEvent = threadId ? INSERT_TO_COMMENT : INSERT_TO_DRAFT;
EventEmitter.emit(insertEvent, value);
export function handlePostDraftSelectionChanged(channelId, cursorPosition) {
return {
type: ViewTypes.POST_DRAFT_SELECTION_CHANGED,
channelId,
cursorPosition,
};
}
export function toggleDMChannel(otherUserId, visible, channelId) {
export function insertToDraft(value) {
return (dispatch, getState) => {
const state = getState();
const channelId = getCurrentChannelId(state);
const threadId = state.entities.posts.selectedPostId;
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,
};
}
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,
});
}
};
}
export function toggleDMChannel(otherUserId, visible) {
return async (dispatch, getState) => {
const state = getState();
const {currentUserId} = state.entities.users;
@@ -350,11 +381,6 @@ export function toggleDMChannel(otherUserId, visible, channelId) {
category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
name: otherUserId,
value: visible,
}, {
user_id: currentUserId,
category: Preferences.CATEGORY_CHANNEL_OPEN_TIME,
name: channelId,
value: Date.now().toString(),
}];
savePreferences(currentUserId, dm)(dispatch, getState);
@@ -412,23 +438,11 @@ 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(selectDefaultChannel(currentTeamId));
}
const {currentTeamId} = getState().entities.teams;
await serviceLeaveChannel(channel.id)(dispatch, getState);
if (channel.isCurrent || reset) {
await selectInitialChannel(currentTeamId)(dispatch, getState);
}
};
}
@@ -468,7 +482,7 @@ export function increasePostVisibility(channelId, focusedPostId) {
const currentPostVisibility = postVisibility[channelId] || 0;
if (loadingPosts[channelId]) {
return false;
return;
}
// Check if we already have the posts that we want to show
@@ -484,7 +498,7 @@ export function increasePostVisibility(channelId, focusedPostId) {
setLoadMorePostsVisible(true),
]));
return true;
return;
}
}
@@ -511,19 +525,15 @@ export function increasePostVisibility(channelId, focusedPostId) {
}];
const posts = result.data;
let hasMorePost = false;
if (posts) {
hasMorePost = posts.order.length >= pageSize;
// make sure to increment the posts visibility
// only if we got results
actions.push(doIncreasePostVisibility(channelId));
actions.push(setLoadMorePostsVisible(hasMorePost));
actions.push(setLoadMorePostsVisible(posts.order.length >= pageSize));
}
dispatch(batchActions(actions));
return hasMorePost;
};
}

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {addChannelMember} from 'mattermost-redux/actions/channels';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {removeChannelMember} from 'mattermost-redux/actions/channels';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {handleSelectChannel, setChannelDisplayName} from './channel';
import {createChannel} from 'mattermost-redux/actions/channels';

View File

@@ -1,7 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {updateMe} from 'mattermost-redux/actions/users';
import {uploadProfileImage, updateMe} from 'mattermost-redux/actions/users';
import {buildFileUploadData} from 'app/utils/file';
export function updateUser(user, success, error) {
return async (dispatch, getState) => {
@@ -15,3 +16,10 @@ export function updateUser(user, success, error) {
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,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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 {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts';

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,15 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {FileTypes} from 'mattermost-redux/action_types';
import {Platform} from 'react-native';
import {uploadFile} from 'mattermost-redux/actions/files';
import {
buildFileUploadData,
encodeHeaderURIStringToUTF8,
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) => {
@@ -20,36 +27,30 @@ export function initUploadFiles(files, rootId) {
clientId,
localPath: fileData.uri,
name: fileData.name,
type: fileData.type,
type: fileData.mimeType,
extension: fileData.extension,
});
fileData.name = encodeHeaderURIStringToUTF8(fileData.name);
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,
});
};
}
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,
const clientIdsArray = clientIds.map((c) => c.clientId);
await uploadFile(channelId, rootId, clientIdsArray, formData, formBoundary)(dispatch, getState);
};
}
@@ -58,6 +59,20 @@ export function retryFileUpload(file, rootId) {
const state = getState();
const channelId = state.entities.channels.currentChannelId;
const formData = new FormData();
const fileData = buildFileUploadData(file);
fileData.uri = file.localPath;
fileData.name = encodeHeaderURIStringToUTF8(fileData.name);
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,
@@ -65,6 +80,8 @@ export function retryFileUpload(file, rootId) {
channelId,
rootId,
});
await uploadFile(channelId, rootId, [file.clientId], formData, formBoundary)(dispatch, getState);
};
}

View File

@@ -1,16 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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 {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
import {Client4} from 'mattermost-redux/client';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {ViewTypes} from 'app/constants';
import {app} from 'app/mattermost';
import {getDeviceTimezone, isTimezoneEnabled} from 'app/utils/timezone';
export function handleLoginIdChanged(loginId) {
return async (dispatch, getState) => {
@@ -32,20 +27,9 @@ export function handlePasswordChanged(password) {
export function handleSuccessfulLogin() {
return async (dispatch, getState) => {
const state = getState();
const config = getConfig(state);
const license = getLicense(state);
const {config, license} = getState().entities.general;
const token = Client4.getToken();
const url = Client4.getUrl();
const deviceToken = state.entities.general.deviceToken;
const currentUserId = getCurrentUserId(state);
app.setAppCredentials(deviceToken, currentUserId, token, url);
const enableTimezone = isTimezoneEnabled(state);
if (enableTimezone) {
dispatch(autoUpdateTimezone(getDeviceTimezone()));
}
dispatch({
type: GeneralTypes.RECEIVED_APP_CREDENTIALS,

View File

@@ -1,57 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import {ViewTypes} from 'app/constants';
import {
handleLoginIdChanged,
handlePasswordChanged,
} from 'app/actions/views/login';
jest.mock('react-native-fetch-blob/fs', () => ({
dirs: {
DocumentDir: () => jest.fn(),
CacheDir: () => jest.fn(),
},
}));
jest.mock('app/mattermost', () => ({
app: {
setAppCredentials: () => jest.fn(),
},
}));
const mockStore = configureStore([thunk]);
describe('Actions.Views.Login', () => {
let store;
beforeEach(() => {
store = mockStore({});
});
test('handleLoginIdChanged', () => {
const loginId = 'email@example.com';
const action = {
type: ViewTypes.LOGIN_ID_CHANGED,
loginId,
};
store.dispatch(handleLoginIdChanged(loginId));
expect(store.getActions()).toEqual([action]);
});
test('handlePasswordChanged', () => {
const password = 'password';
const action = {
type: ViewTypes.PASSWORD_CHANGED,
password,
};
store.dispatch(handlePasswordChanged(password));
expect(store.getActions()).toEqual([action]);
});
});

View File

@@ -1,34 +1,34 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {getDirectChannelName} from 'mattermost-redux/utils/channel_utils';
import {createDirectChannel, createGroupChannel} from 'mattermost-redux/actions/channels';
import {getProfilesByIds, getStatusesByIds} from 'mattermost-redux/actions/users';
import {handleSelectChannel, toggleDMChannel, toggleGMChannel} from 'app/actions/views/channel';
export function makeDirectChannel(otherUserId, switchToChannel = true) {
export function makeDirectChannel(otherUserId) {
return async (dispatch, getState) => {
const state = getState();
const {currentUserId} = state.entities.users;
const channelName = getDirectChannelName(currentUserId, otherUserId);
const {channels, myMembers} = state.entities.channels;
dispatch(getProfilesByIds([otherUserId]));
dispatch(getStatusesByIds([otherUserId]));
getProfilesByIds([otherUserId])(dispatch, getState);
getStatusesByIds([otherUserId])(dispatch, getState);
let result;
let channel = Object.values(channels).find((c) => c.name === channelName);
if (channel && myMembers[channel.id]) {
result = {data: channel};
dispatch(toggleDMChannel(otherUserId, 'true', channel.id));
toggleDMChannel(otherUserId, 'true')(dispatch, getState);
} else {
result = await createDirectChannel(currentUserId, otherUserId)(dispatch, getState);
channel = result.data;
}
if (channel && switchToChannel) {
dispatch(handleSelectChannel(channel.id));
if (channel) {
handleSelectChannel(channel.id)(dispatch, getState);
}
return result;
@@ -40,15 +40,15 @@ export function makeGroupChannel(otherUserIds) {
const state = getState();
const {currentUserId} = state.entities.users;
dispatch(getProfilesByIds(otherUserIds));
dispatch(getStatusesByIds(otherUserIds));
getProfilesByIds(otherUserIds)(dispatch, getState);
getStatusesByIds(otherUserIds)(dispatch, getState);
const result = await createGroupChannel([currentUserId, ...otherUserIds])(dispatch, getState);
const channel = result.data;
if (channel) {
dispatch(toggleGMChannel(channel.id, 'true'));
dispatch(handleSelectChannel(channel.id));
toggleGMChannel(channel.id, 'true')(dispatch, getState);
handleSelectChannel(channel.id)(dispatch, getState);
}
return result;

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,30 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {GeneralTypes, PostTypes} from 'mattermost-redux/action_types';
import {Client4} from 'mattermost-redux/client';
import {General} from 'mattermost-redux/constants';
import {fetchMyChannelsAndMembers, markChannelAsRead} from 'mattermost-redux/actions/channels';
import {markChannelAsRead} from 'mattermost-redux/actions/channels';
import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from 'mattermost-redux/actions/general';
import {getPosts} from 'mattermost-redux/actions/posts';
import {getMyTeams, getMyTeamMembers, selectTeam} from 'mattermost-redux/actions/teams';
import {ViewTypes} from 'app/constants';
import {recordTime} from 'app/utils/segment';
import {
handleSelectChannel,
setChannelDisplayName,
retryGetPostsAction,
} from 'app/actions/views/channel';
export function startDataCleanup() {
return async (dispatch, getState) => {
dispatch({
type: ViewTypes.DATA_CLEANUP,
payload: getState(),
});
};
}
export function loadConfigAndLicense() {
return async (dispatch, getState) => {
const {currentUserId} = getState().entities.users;
@@ -54,41 +46,35 @@ export function loadFromPushNotification(notification) {
const state = getState();
const {data} = notification;
const {currentTeamId, teams, myMembers: myTeamMembers} = state.entities.teams;
const {currentChannelId, channels} = state.entities.channels;
const {currentChannelId} = state.entities.channels;
const channelId = data.channel_id;
// when the notification does not have a team id is because its from a DM or GM
const teamId = data.team_id || currentTeamId;
// load any missing data
const loading = [];
//verify that we have the team loaded
if (teamId && (!teams[teamId] || !myTeamMembers[teamId])) {
loading.push(dispatch(getMyTeams()));
loading.push(dispatch(getMyTeamMembers()));
}
if (channelId && !channels[channelId]) {
loading.push(dispatch(fetchMyChannelsAndMembers(teamId)));
}
if (loading.length > 0) {
await Promise.all(loading);
await Promise.all([
getMyTeams()(dispatch, getState),
getMyTeamMembers()(dispatch, getState),
]);
}
// when the notification is from a team other than the current team
if (teamId !== currentTeamId) {
dispatch(selectTeam({id: teamId}));
selectTeam({id: teamId})(dispatch, getState);
}
// mark channel as read
dispatch(markChannelAsRead(channelId, channelId === currentChannelId ? null : currentChannelId, false));
if (channelId !== currentChannelId) {
// 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
dispatch(markChannelAsRead(channelId, currentChannelId, false));
markChannelAsRead(channelId, currentChannelId, false)(dispatch, getState);
dispatch(setChannelDisplayName(''));
dispatch(handleSelectChannel(channelId));
handleSelectChannel(channelId)(dispatch, getState);
}
};
}

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';

View File

@@ -1,41 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {batchActions} from 'redux-batched-actions';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import {GeneralTypes} from 'mattermost-redux/action_types';
import {ViewTypes} from 'app/constants';
import {handleServerUrlChanged} from 'app/actions/views/select_server';
jest.mock('react-native-fetch-blob/fs', () => ({
dirs: {
DocumentDir: () => jest.fn(),
CacheDir: () => jest.fn(),
},
}));
const mockStore = configureStore([thunk]);
describe('Actions.Views.SelectServer', () => {
let store;
beforeEach(() => {
store = mockStore({});
});
test('handleServerUrlChanged', async () => {
const serverUrl = 'https://mattermost.example.com';
const actions = batchActions([
{type: GeneralTypes.CLIENT_CONFIG_RESET},
{type: GeneralTypes.CLIENT_LICENSE_RESET},
{type: ViewTypes.SERVER_URL_CHANGED, serverUrl},
]);
store.dispatch(handleServerUrlChanged(serverUrl));
expect(store.getActions()).toEqual([actions]);
});
});

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {batchActions} from 'redux-batched-actions';
@@ -11,7 +11,6 @@ import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'
import {NavigationTypes} from 'app/constants';
import {setChannelDisplayName} from './channel';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
export function handleTeamChange(teamId, selectChannel = true) {
return async (dispatch, getState) => {
@@ -21,7 +20,10 @@ export function handleTeamChange(teamId, selectChannel = true) {
return;
}
const actions = [setChannelDisplayName(''), {type: TeamTypes.SELECT_TEAM, data: teamId}];
const actions = [
setChannelDisplayName(''),
{type: TeamTypes.SELECT_TEAM, data: teamId},
];
if (selectChannel) {
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: ''});
@@ -37,25 +39,14 @@ export function handleTeamChange(teamId, selectChannel = true) {
};
}
export function selectDefaultTeam() {
export function selectFirstAvailableTeam() {
return async (dispatch, getState) => {
const state = getState();
const {ExperimentalPrimaryTeam} = getConfig(state);
const {teams: allTeams, myMembers} = state.entities.teams;
const {teams: allTeams, myMembers} = getState().entities.teams;
const teams = Object.keys(myMembers).map((key) => allTeams[key]);
const firstTeam = Object.values(teams).sort((a, b) => a.display_name.localeCompare(b.display_name))[0];
let defaultTeam;
if (ExperimentalPrimaryTeam) {
defaultTeam = teams.find((t) => t.name === ExperimentalPrimaryTeam.toLowerCase());
}
if (!defaultTeam) {
defaultTeam = Object.values(teams).sort((a, b) => a.display_name.localeCompare(b.display_name))[0];
}
if (defaultTeam) {
handleTeamChange(defaultTeam.id)(dispatch, getState);
if (firstTeam) {
handleTeamChange(firstTeam.id)(dispatch, getState);
} else {
EventEmitter.emit(NavigationTypes.NAVIGATION_NO_TEAMS);
}
@@ -64,5 +55,5 @@ export function selectDefaultTeam() {
export default {
handleTeamChange,
selectDefaultTeam,
selectFirstAvailableTeam,
};

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';

View File

@@ -1,53 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import {ViewTypes} from 'app/constants';
import {
handleCommentDraftChanged,
handleCommentDraftSelectionChanged,
} from 'app/actions/views/thread';
jest.mock('react-native-fetch-blob/fs', () => ({
dirs: {
DocumentDir: () => jest.fn(),
CacheDir: () => jest.fn(),
},
}));
const mockStore = configureStore([thunk]);
describe('Actions.Views.Thread', () => {
let store;
beforeEach(() => {
store = mockStore({});
});
test('handleCommentDraftChanged', async () => {
const rootId = '1234';
const draft = 'draft1';
const action = {
type: ViewTypes.COMMENT_DRAFT_CHANGED,
rootId,
draft,
};
await store.dispatch(handleCommentDraftChanged(rootId, draft));
expect(store.getActions()).toEqual([action]);
});
test('handleCommentDraftSelectionChanged', async () => {
const rootId = '1234';
const cursorPosition = 'position';
const action = {
type: ViewTypes.COMMENT_DRAFT_SELECTION_CHANGED,
rootId,
cursorPosition,
};
await store.dispatch(handleCommentDraftSelectionChanged(rootId, cursorPosition));
expect(store.getActions()).toEqual([action]);
});
});

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {userTyping as wsUserTyping} from 'mattermost-redux/actions/websocket';

View File

@@ -1,259 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable global-require*/
import {AsyncStorage, NativeModules} from 'react-native';
import {setGenericPassword, getGenericPassword, resetGenericPassword} from 'react-native-keychain';
import {loadMe} from 'mattermost-redux/actions/users';
import {Client4} from 'mattermost-redux/client';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {ViewTypes} from 'app/constants';
import tracker from 'app/utils/time_tracker';
import {getCurrentLocale} from 'app/selectors/i18n';
import {getTranslations as getLocalTranslations} from 'app/i18n';
import {store, handleManagedConfig} from 'app/mattermost';
import avoidNativeBridge from 'app/utils/avoid_native_bridge';
const {Initialization} = NativeModules;
const TOOLBAR_BACKGROUND = 'TOOLBAR_BACKGROUND';
const TOOLBAR_TEXT_COLOR = 'TOOLBAR_TEXT_COLOR';
const APP_BACKGROUND = 'APP_BACKGROUND';
export default class App {
constructor() {
// Usage: app.js
this.shouldRelaunchWhenActive = false;
this.inBackgroundSince = null;
// Usage: screen/entry.js
this.startAppFromPushNotification = false;
this.isNotificationsConfigured = false;
this.allowOtherServers = true;
this.appStarted = false;
this.emmEnabled = false;
this.performingEMMAuthentication = false;
this.translations = null;
this.toolbarBackground = null;
this.toolbarTextColor = null;
this.appBackground = null;
// Usage utils/push_notifications.js
this.replyNotificationData = null;
this.deviceToken = null;
// Usage credentials
this.currentUserId = null;
this.token = null;
this.url = null;
this.getStartupThemes();
this.getAppCredentials();
}
getTranslations = () => {
if (this.translations) {
return this.translations;
}
const state = store.getState();
const locale = getCurrentLocale(state);
this.translations = getLocalTranslations(locale);
return this.translations;
};
getAppCredentials = async () => {
try {
const credentials = await avoidNativeBridge(
() => {
return Initialization.credentialsExist;
},
() => {
return Initialization.credentials;
},
() => {
this.waitForRehydration = true;
return getGenericPassword();
}
);
if (credentials) {
const usernameParsed = credentials.username.split(',');
const passwordParsed = credentials.password.split(',');
// username == deviceToken, currentUserId
// password == token, url
if (usernameParsed.length === 2 && passwordParsed.length === 2) {
const [deviceToken, currentUserId] = usernameParsed;
const [token, url] = passwordParsed;
// if for any case the url and the token aren't valid proceed with re-hydration
if (url && url !== 'undefined' && token && token !== 'undefined') {
this.deviceToken = deviceToken;
this.currentUserId = currentUserId;
this.token = token;
this.url = url;
Client4.setUrl(url);
Client4.setToken(token);
} else {
this.waitForRehydration = true;
}
}
}
} catch (error) {
return null;
}
return null;
};
getStartupThemes = async () => {
try {
const [
toolbarBackground,
toolbarTextColor,
appBackground,
] = await avoidNativeBridge(
() => {
return Initialization.themesExist;
},
() => {
return [
Initialization.toolbarBackground,
Initialization.toolbarTextColor,
Initialization.appBackground,
];
},
() => {
return Promise.all([
AsyncStorage.getItem(TOOLBAR_BACKGROUND),
AsyncStorage.getItem(TOOLBAR_TEXT_COLOR),
AsyncStorage.getItem(APP_BACKGROUND),
]);
}
);
if (toolbarBackground) {
this.toolbarBackground = toolbarBackground;
this.toolbarTextColor = toolbarTextColor;
this.appBackground = appBackground;
}
} catch (error) {
return null;
}
return null;
};
setPerformingEMMAuthentication = (authenticating) => {
this.performingEMMAuthentication = authenticating;
};
setAppCredentials = (deviceToken, currentUserId, token, url) => {
if (!currentUserId) {
return;
}
const username = `${deviceToken}, ${currentUserId}`;
const password = `${token},${url}`;
if (this.waitForRehydration) {
this.waitForRehydration = false;
this.token = token;
this.url = url;
}
// Only save to keychain if the url and token are set
if (url && token) {
setGenericPassword(username, password);
}
};
setStartupThemes = (toolbarBackground, toolbarTextColor, appBackground) => {
AsyncStorage.setItem(TOOLBAR_BACKGROUND, toolbarBackground);
AsyncStorage.setItem(TOOLBAR_TEXT_COLOR, toolbarTextColor);
AsyncStorage.setItem(APP_BACKGROUND, appBackground);
};
setStartAppFromPushNotification = (startAppFromPushNotification) => {
this.startAppFromPushNotification = startAppFromPushNotification;
};
setIsNotificationsConfigured = (isNotificationsConfigured) => {
this.isNotificationsConfigured = isNotificationsConfigured;
};
setAllowOtherServers = (allowOtherServers) => {
this.allowOtherServers = allowOtherServers;
};
setAppStarted = (appStarted) => {
this.appStarted = appStarted;
};
setEMMEnabled = (emmEnabled) => {
this.emmEnabled = emmEnabled;
};
setDeviceToken = (deviceToken) => {
this.deviceToken = deviceToken;
};
setReplyNotificationData = (replyNotificationData) => {
this.replyNotificationData = replyNotificationData;
};
setInBackgroundSince = (inBackgroundSince) => {
this.inBackgroundSince = inBackgroundSince;
};
setShouldRelaunchWhenActive = (shouldRelaunchWhenActive) => {
this.shouldRelaunchWhenActive = shouldRelaunchWhenActive;
};
clearNativeCache = () => {
resetGenericPassword();
AsyncStorage.multiRemove([
TOOLBAR_BACKGROUND,
TOOLBAR_TEXT_COLOR,
APP_BACKGROUND,
]);
};
launchApp = async () => {
const shouldStart = await handleManagedConfig();
if (shouldStart) {
this.startApp();
}
};
startApp = () => {
if (this.appStarted || this.waitForRehydration) {
return;
}
const {dispatch} = store;
let screen = 'SelectServer';
if (this.token && this.url) {
screen = 'Channel';
tracker.initialLoad = Date.now();
dispatch(loadMe());
}
switch (screen) {
case 'SelectServer':
EventEmitter.emit(ViewTypes.LAUNCH_LOGIN, true);
break;
case 'Channel':
EventEmitter.emit(ViewTypes.LAUNCH_CHANNEL, true);
break;
}
this.setStartAppFromPushNotification(false);
this.setAppStarted(true);
}
}

View File

@@ -1,385 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AnnouncementBanner should match snapshot 1`] = `
ShallowWrapper {
"length": 1,
Symbol(enzyme.__root__): [Circular],
Symbol(enzyme.__unrendered__): <AnnouncementBanner
bannerColor="#ddd"
bannerDismissed={false}
bannerEnabled={true}
bannerText="Banner Text"
bannerTextColor="#fff"
/>,
Symbol(enzyme.__renderer__): Object {
"batchedUpdates": [Function],
"getNode": [Function],
"render": [Function],
"simulateEvent": [Function],
"unmount": [Function],
},
Symbol(enzyme.__node__): Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": <TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
}
}
>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"flex": 1,
"fontSize": 14,
"marginRight": 5,
},
Object {
"color": "#fff",
},
]
}
>
<RemoveMarkdown
value="Banner Text"
/>
</Text>
<Icon
allowFontScaling={false}
color="#fff"
name="info"
size={16}
/>
</TouchableOpacity>,
"style": Array [
Object {
"overflow": "hidden",
"paddingHorizontal": 10,
"position": "absolute",
"top": 0,
"width": "100%",
},
Object {
"backgroundColor": "#ddd",
"height": 0,
},
],
},
"ref": null,
"rendered": Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"activeOpacity": 0.2,
"children": Array [
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"flex": 1,
"fontSize": 14,
"marginRight": 5,
},
Object {
"color": "#fff",
},
]
}
>
<RemoveMarkdown
value="Banner Text"
/>
</Text>,
<Icon
allowFontScaling={false}
color="#fff"
name="info"
size={16}
/>,
],
"onPress": [Function],
"style": Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
},
},
"ref": null,
"rendered": Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"accessible": true,
"allowFontScaling": true,
"children": <RemoveMarkdown
value="Banner Text"
/>,
"ellipsizeMode": "tail",
"numberOfLines": 1,
"style": Array [
Object {
"flex": 1,
"fontSize": 14,
"marginRight": 5,
},
Object {
"color": "#fff",
},
],
},
"ref": null,
"rendered": Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"value": "Banner Text",
},
"ref": null,
"rendered": null,
"type": [Function],
},
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"allowFontScaling": false,
"color": "#fff",
"name": "info",
"size": 16,
},
"ref": null,
"rendered": null,
"type": [Function],
},
],
"type": [Function],
},
"type": [Function],
},
Symbol(enzyme.__nodes__): Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": <TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
}
}
>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"flex": 1,
"fontSize": 14,
"marginRight": 5,
},
Object {
"color": "#fff",
},
]
}
>
<RemoveMarkdown
value="Banner Text"
/>
</Text>
<Icon
allowFontScaling={false}
color="#fff"
name="info"
size={16}
/>
</TouchableOpacity>,
"style": Array [
Object {
"overflow": "hidden",
"paddingHorizontal": 10,
"position": "absolute",
"top": 0,
"width": "100%",
},
Object {
"backgroundColor": "#ddd",
"height": 0,
},
],
},
"ref": null,
"rendered": Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"activeOpacity": 0.2,
"children": Array [
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"flex": 1,
"fontSize": 14,
"marginRight": 5,
},
Object {
"color": "#fff",
},
]
}
>
<RemoveMarkdown
value="Banner Text"
/>
</Text>,
<Icon
allowFontScaling={false}
color="#fff"
name="info"
size={16}
/>,
],
"onPress": [Function],
"style": Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
},
},
"ref": null,
"rendered": Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"accessible": true,
"allowFontScaling": true,
"children": <RemoveMarkdown
value="Banner Text"
/>,
"ellipsizeMode": "tail",
"numberOfLines": 1,
"style": Array [
Object {
"flex": 1,
"fontSize": 14,
"marginRight": 5,
},
Object {
"color": "#fff",
},
],
},
"ref": null,
"rendered": Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"value": "Banner Text",
},
"ref": null,
"rendered": null,
"type": [Function],
},
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"allowFontScaling": false,
"color": "#fff",
"name": "info",
"size": 16,
},
"ref": null,
"rendered": null,
"type": [Function],
},
],
"type": [Function],
},
"type": [Function],
},
],
Symbol(enzyme.__options__): Object {
"adapter": ReactSixteenAdapter {
"options": Object {
"enableComponentDidUpdateOnSetState": true,
},
},
},
}
`;
exports[`AnnouncementBanner should match snapshot 2`] = `
ShallowWrapper {
"length": 1,
Symbol(enzyme.__root__): [Circular],
Symbol(enzyme.__unrendered__): <AnnouncementBanner
bannerColor="#ddd"
bannerDismissed={false}
bannerEnabled={false}
bannerText="Banner Text"
bannerTextColor="#fff"
/>,
Symbol(enzyme.__renderer__): Object {
"batchedUpdates": [Function],
"getNode": [Function],
"render": [Function],
"simulateEvent": [Function],
"unmount": [Function],
},
Symbol(enzyme.__node__): null,
Symbol(enzyme.__nodes__): Array [
null,
],
Symbol(enzyme.__options__): Object {
"adapter": ReactSixteenAdapter {
"options": Object {
"enableComponentDidUpdateOnSetState": true,
},
},
},
}
`;

View File

@@ -1,9 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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,
@@ -12,19 +13,19 @@ import {
import {intlShape} from 'react-intl';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import RemoveMarkdown from 'app/components/remove_markdown';
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,
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
};
static contextTypes = {
@@ -44,31 +45,36 @@ export default class AnnouncementBanner extends PureComponent {
componentWillReceiveProps(nextProps) {
if (this.props.bannerText !== nextProps.bannerText ||
this.props.bannerEnabled !== nextProps.bannerEnabled ||
this.props.bannerDismissed !== nextProps.bannerDismissed
) {
this.props.bannerDismissed !== nextProps.bannerDismissed) {
const showBanner = nextProps.bannerEnabled && !nextProps.bannerDismissed && Boolean(nextProps.bannerText);
this.toggleBanner(showBanner);
}
}
handlePress = () => {
const {navigator, theme} = this.props;
handleDismiss = () => {
const {actions, bannerText} = this.props;
actions.dismissBanner(bannerText);
};
navigator.push({
screen: 'ExpandedAnnouncementBanner',
title: this.context.intl.formatMessage({
id: 'mobile.announcement_banner.title',
defaultMessage: 'Announcement',
}),
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
});
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) => {
@@ -80,24 +86,15 @@ export default class AnnouncementBanner extends PureComponent {
};
render() {
if (!this.props.bannerEnabled) {
return null;
}
const {bannerHeight} = this.state;
const {
bannerColor,
bannerText,
bannerTextColor,
} = this.props;
const bannerStyle = {
backgroundColor: bannerColor,
backgroundColor: this.props.bannerColor,
height: bannerHeight,
};
const bannerTextStyle = {
color: bannerTextColor,
color: this.props.bannerTextColor,
};
return (
@@ -113,10 +110,10 @@ export default class AnnouncementBanner extends PureComponent {
numberOfLines={1}
style={[style.bannerText, bannerTextStyle]}
>
<RemoveMarkdown value={bannerText}/>
{this.props.bannerText}
</Text>
<MaterialIcons
color={bannerTextColor}
color={this.props.bannerTextColor}
name='info'
size={16}
/>

View File

@@ -1,32 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {configure, shallow} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({adapter: new Adapter()});
import AnnouncementBanner from './announcement_banner.js';
jest.useFakeTimers();
describe('AnnouncementBanner', () => {
const baseProps = {
bannerColor: '#ddd',
bannerDismissed: false,
bannerEnabled: true,
bannerText: 'Banner Text',
bannerTextColor: '#fff',
};
test('should match snapshot', () => {
const wrapper = shallow(
<AnnouncementBanner {...baseProps}/>
);
expect(wrapper).toMatchSnapshot();
wrapper.setProps({bannerEnabled: false});
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -1,10 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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 {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {dismissBanner} from 'app/actions/views/announcement';
import AnnouncementBanner from './announcement_banner';
@@ -14,13 +16,21 @@ function mapStateToProps(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 || '#000',
theme: getTheme(state),
bannerTextColor: config.BannerTextColor,
};
}
export default connect(mapStateToProps)(AnnouncementBanner);
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
dismissBanner,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AnnouncementBanner);

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';
@@ -8,7 +8,7 @@ import Svg, {
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,

View File

@@ -1,13 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
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 CustomPropTypes from 'app/constants/custom_prop_types';
import mattermostManaged from 'app/mattermost_managed';
@@ -20,29 +18,30 @@ export default class AtMention extends React.PureComponent {
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,
};
}
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,
});
}
}
@@ -56,7 +55,7 @@ export default class AtMention extends React.PureComponent {
animated: true,
backButtonTitle: '',
passProps: {
userId: this.state.user.id,
userId: this.state.id,
},
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
@@ -78,7 +77,11 @@ export default class AtMention extends React.PureComponent {
while (mentionName.length > 0) {
if (props.usersByUsername.hasOwnProperty(mentionName)) {
return 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
@@ -111,22 +114,22 @@ export default class AtMention extends React.PureComponent {
}
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
@@ -135,7 +138,7 @@ export default class AtMention extends React.PureComponent {
onLongPress={this.handleLongPress}
>
<Text style={mentionStyle}>
{'@' + displayUsername(user, teammateNameDisplay)}
{'@' + username}
</Text>
{suffix}
</Text>

View File

@@ -1,11 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
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';
@@ -13,7 +13,6 @@ function mapStateToProps(state) {
return {
theme: getTheme(state),
usersByUsername: getUsersByUsername(state),
teammateNameDisplay: getTeammateNameDisplaySetting(state),
};
}

View File

@@ -1,30 +1,25 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {injectIntl, intlShape} from 'react-intl';
import {
Alert,
NativeModules,
Platform,
StyleSheet,
TouchableOpacity,
} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import {DocumentPicker} from 'react-native-document-picker';
import ImagePicker from 'react-native-image-picker';
import Permissions from 'react-native-permissions';
import {PermissionTypes} from 'app/constants';
import {changeOpacity} from 'app/utils/theme';
const ShareExtension = NativeModules.MattermostShare;
export default class AttachmentButton extends PureComponent {
class AttachmentButton extends PureComponent {
static propTypes = {
blurTextBox: PropTypes.func.isRequired,
children: PropTypes.node,
fileCount: PropTypes.number,
intl: intlShape.isRequired,
maxFileCount: PropTypes.number.isRequired,
navigator: PropTypes.object.isRequired,
onShowFileMaxWarning: PropTypes.func,
@@ -37,12 +32,8 @@ export default class AttachmentButton extends PureComponent {
maxFileCount: 5,
};
static contextTypes = {
intl: intlShape.isRequired,
};
attachFileFromCamera = async () => {
const {formatMessage} = this.context.intl;
const {formatMessage} = this.props.intl;
const options = {
quality: 1.0,
noData: true,
@@ -81,7 +72,7 @@ export default class AttachmentButton extends PureComponent {
};
attachFileFromLibrary = () => {
const {formatMessage} = this.context.intl;
const {formatMessage} = this.props.intl;
const options = {
quality: 1.0,
noData: true,
@@ -116,7 +107,7 @@ export default class AttachmentButton extends PureComponent {
};
attachVideoFromLibraryAndroid = () => {
const {formatMessage} = this.context.intl;
const {formatMessage} = this.props.intl;
const options = {
quality: 1.0,
mediaType: 'video',
@@ -147,38 +138,9 @@ export default class AttachmentButton extends PureComponent {
});
};
attachFileFromFiles = async () => {
const hasPermission = await this.hasStoragePermission();
if (hasPermission) {
DocumentPicker.show({
filetype: [Platform.OS === 'ios' ? 'public.item' : '*/*'],
}, async (error, res) => {
if (error) {
return;
}
if (Platform.OS === 'android') {
// For android we need to retrieve the realPath in case the file being imported is from the cloud
const newUri = await ShareExtension.getFilePath(res.uri);
if (newUri.filePath) {
res.uri = newUri.filePath;
} else {
return;
}
}
// Decode file uri to get the actual path
res.uri = decodeURIComponent(res.uri);
this.uploadFiles([res]);
});
}
};
hasPhotoPermission = async () => {
if (Platform.OS === 'ios') {
const {formatMessage} = this.context.intl;
const {formatMessage} = this.props.intl;
let permissionRequest;
const hasPermissionToStorage = await Permissions.check('photo');
@@ -229,59 +191,6 @@ export default class AttachmentButton extends PureComponent {
return true;
};
hasStoragePermission = async () => {
if (Platform.OS === 'android') {
const {formatMessage} = this.context.intl;
let permissionRequest;
const hasPermissionToStorage = await Permissions.check('storage');
switch (hasPermissionToStorage) {
case PermissionTypes.UNDETERMINED:
permissionRequest = await Permissions.request('storage');
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
return false;
}
break;
case PermissionTypes.DENIED: {
const canOpenSettings = await Permissions.canOpenSettings();
let grantOption = null;
if (canOpenSettings) {
grantOption = {
text: formatMessage({
id: 'mobile.android.permission_denied_retry',
defaultMessage: 'Set permission',
}),
onPress: () => Permissions.openSettings(),
};
}
Alert.alert(
formatMessage({
id: 'mobile.android.storage_permission_denied_title',
defaultMessage: 'File Storage access is required',
}),
formatMessage({
id: 'mobile.android.storage_permission_denied_description',
defaultMessage: 'To upload images from your Android device, please change your permission settings.',
}),
[
grantOption,
{
text: formatMessage({
id: 'mobile.android.permission_denied_dismiss',
defaultMessage: 'Dismiss',
}),
},
]
);
return false;
}
}
}
return true;
};
uploadFiles = (images) => {
this.props.uploadFiles(images);
};
@@ -339,15 +248,6 @@ export default class AttachmentButton extends PureComponent {
});
}
options.items.push({
action: () => this.handleFileAttachmentOption(this.attachFileFromFiles),
text: {
id: 'mobile.file_upload.browse',
defaultMessage: 'Browse Files',
},
icon: 'file',
});
this.props.navigator.showModal({
screen: 'OptionsModal',
title: '',
@@ -412,3 +312,4 @@ const style = StyleSheet.create({
},
});
export default injectIntl(AttachmentButton);

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';
@@ -166,7 +166,7 @@ export default class AtMention extends PureComponent {
completedDraft += value.substring(cursorPosition);
}
onChangeText(completedDraft);
onChangeText(completedDraft, true);
this.setState({mentionComplete: true});
};

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';
@@ -19,7 +19,6 @@ 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,
@@ -30,10 +29,10 @@ export default class Autocomplete extends PureComponent {
static defaultProps = {
isSearch: false,
cursorPosition: 0,
};
state = {
cursorPosition: 0,
atMentionCount: 0,
channelMentionCount: 0,
emojiCount: 0,
@@ -41,6 +40,12 @@ export default class Autocomplete extends PureComponent {
keyboardOffset: 0,
};
handleSelectionChange = (event) => {
this.setState({
cursorPosition: event.nativeEvent.selection.end,
});
};
handleAtMentionCountChange = (atMentionCount) => {
this.setState({atMentionCount});
};
@@ -70,11 +75,11 @@ export default class Autocomplete extends PureComponent {
keyboardDidShow = (e) => {
const {height} = e.endCoordinates;
this.setState({keyboardOffset: height});
};
}
keyboardDidHide = () => {
this.setState({keyboardOffset: 0});
};
}
listHeight() {
let offset = Platform.select({ios: 65, android: 75});
@@ -106,21 +111,23 @@ export default class Autocomplete extends PureComponent {
}
}
const listHeight = this.listHeight();
return (
<View style={wrapperStyle}>
<View style={containerStyle}>
<AtMention
listHeight={listHeight}
cursorPosition={this.state.cursorPosition}
onResultCountChange={this.handleAtMentionCountChange}
{...this.props}
/>
<ChannelMention
listHeight={listHeight}
cursorPosition={this.state.cursorPosition}
onResultCountChange={this.handleChannelMentionCountChange}
{...this.props}
/>
<EmojiSuggestion
cursorPosition={this.state.cursorPosition}
onResultCountChange={this.handleEmojiCountChange}
{...this.props}
/>

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';
@@ -82,9 +82,9 @@ export default class EmojiSuggestion extends Component {
return;
}
if (this.matchTerm.length) {
if (this.props.emojis !== nextProps.emojis) {
this.handleFuzzySearch(this.matchTerm, nextProps);
} else {
} else if (!this.matchTerm.length) {
const initialEmojis = [...nextProps.emojis];
initialEmojis.splice(0, 300);
const data = initialEmojis.sort();

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {createSelector} from 'reselect';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';

View File

@@ -1,35 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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 {
BackHandler,
InteractionManager,
Keyboard,
Platform,
StyleSheet,
View,
} from 'react-native';
import {intlShape} from 'react-intl';
import DrawerLayout from 'react-native-drawer-layout';
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;
const DRAWER_INITIAL_OFFSET = 40;
const DRAWER_LANDSCAPE_OFFSET = 150;
export default class ChannelSidebar extends Component {
export default class ChannelDrawer extends Component {
static propTypes = {
actions: PropTypes.shape({
getTeams: PropTypes.func.isRequired,
handleSelectChannel: PropTypes.func.isRequired,
markChannelAsViewed: PropTypes.func.isRequired,
makeDirectChannel: PropTypes.func.isRequired,
markChannelAsRead: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
setChannelLoading: PropTypes.func.isRequired,
}).isRequired,
@@ -37,18 +49,16 @@ export default class ChannelSidebar extends Component {
children: PropTypes.node,
currentTeamId: PropTypes.string.isRequired,
currentUserId: PropTypes.string.isRequired,
deviceWidth: PropTypes.number.isRequired,
isLandscape: PropTypes.bool.isRequired,
isTablet: PropTypes.bool.isRequired,
intl: PropTypes.object.isRequired,
navigator: PropTypes.object,
teamsCount: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired,
};
static contextTypes = {
intl: intlShape.isRequired,
};
closeHandle = null;
openHandle = null;
swiperIndex = 1;
constructor(props) {
@@ -58,12 +68,7 @@ export default class ChannelSidebar extends Component {
if (props.isLandscape || props.isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.drawerOpened = false;
this.state = {
show: false,
lockMode: 'unlocked',
openDrawerOffset,
};
}
@@ -74,7 +79,6 @@ export default class ChannelSidebar extends Component {
componentDidMount() {
EventEmitter.on('close_channel_drawer', this.closeChannelDrawer);
EventEmitter.on('renderDrawer', this.handleShowDrawerContent);
EventEmitter.on(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack);
}
@@ -93,41 +97,36 @@ export default class ChannelSidebar extends Component {
}
shouldComponentUpdate(nextProps, nextState) {
const {currentTeamId, deviceWidth, isLandscape, teamsCount} = this.props;
const {currentTeamId, isLandscape, teamsCount} = this.props;
const {openDrawerOffset} = this.state;
if (nextState.openDrawerOffset !== openDrawerOffset || nextState.show !== this.state.show) {
if (nextState.openDrawerOffset !== openDrawerOffset) {
return true;
}
return nextProps.currentTeamId !== currentTeamId ||
nextProps.isLandscape !== isLandscape || nextProps.deviceWidth !== deviceWidth ||
nextProps.teamsCount !== teamsCount || this.state.lockMode !== nextState.lockMode;
nextProps.isLandscape !== isLandscape ||
nextProps.teamsCount !== teamsCount;
}
componentWillUnmount() {
EventEmitter.off('close_channel_drawer', this.closeChannelDrawer);
EventEmitter.off(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
EventEmitter.off('renderDrawer', this.handleShowDrawerContent);
BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack);
}
handleAndroidBack = () => {
if (this.drawerOpened && this.refs.drawer) {
this.refs.drawer.closeDrawer();
if (this.refs.drawer && this.refs.drawer.isOpened()) {
this.refs.drawer.close();
return true;
}
return false;
};
handleShowDrawerContent = () => {
this.setState({show: true});
};
closeChannelDrawer = () => {
if (this.drawerOpened && this.refs.drawer) {
this.refs.drawer.closeDrawer();
if (this.refs.drawer && this.refs.drawer.isOpened()) {
this.refs.drawer.close();
}
};
@@ -136,16 +135,53 @@ export default class ChannelSidebar extends Component {
};
handleDrawerClose = () => {
this.drawerOpened = false;
this.resetDrawer();
if (this.closeHandle) {
InteractionManager.clearInteractionHandle(this.closeHandle);
this.closeHandle = null;
}
};
handleDrawerCloseStart = () => {
if (!this.closeHandle) {
this.closeHandle = InteractionManager.createInteractionHandle();
}
};
handleDrawerOpen = () => {
this.drawerOpened = true;
if (this.state.openDrawerOffset !== 0) {
Keyboard.dismiss();
}
if (this.openHandle) {
InteractionManager.clearInteractionHandle(this.openHandle);
this.openHandle = null;
}
};
handleDrawerOpenStart = () => {
if (!this.openHandle) {
this.openHandle = InteractionManager.createInteractionHandle();
}
};
handleDrawerTween = (ratio) => {
const opacity = (ratio / 2);
EventEmitter.emit('drawer_opacity', opacity);
return {
mainOverlay: {
backgroundColor: this.props.theme.centerChannelBg,
elevation: 3,
opacity,
},
drawerOverlay: {
backgroundColor: ratio ? '#000' : '#FFF',
opacity: ratio ? (1 - ratio) / 2 : 1,
},
};
};
handleUpdateTitle = (channel) => {
@@ -156,11 +192,11 @@ export default class ChannelSidebar extends Component {
this.props.actions.setChannelDisplayName(channelName);
};
openChannelSidebar = () => {
openChannelDrawer = () => {
this.props.blurPostTextBox();
if (this.refs.drawer) {
this.refs.drawer.openDrawer();
if (this.refs.drawer && !this.refs.drawer.isOpened()) {
this.refs.drawer.open();
}
};
@@ -170,25 +206,38 @@ export default class ChannelSidebar extends Component {
} = this.props;
const {
handleSelectChannel,
markChannelAsRead,
setChannelLoading,
setChannelDisplayName,
markChannelAsViewed,
} = actions;
tracker.channelSwitch = Date.now();
this.closeChannelDrawer();
setChannelLoading(channel.id !== currentChannelId);
setChannelDisplayName(channel.display_name);
EventEmitter.emit('switch_channel', channel, currentChannelId);
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);
}
});
});
};
joinChannel = async (channel, currentChannelId) => {
const {intl} = this.context;
const {
actions,
currentTeamId,
currentUserId,
intl,
} = this.props;
const {
@@ -197,28 +246,27 @@ export default class ChannelSidebar extends Component {
} = actions;
const displayValue = {displayName: channel.display_name};
const utils = require('app/utils/general');
let result;
if (channel.type === General.DM_CHANNEL) {
result = await makeDirectChannel(channel.id, false);
result = await makeDirectChannel(channel.id);
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.",
};
utils.alertErrorWithFallback(intl, result.error, dmFailedMessage, displayValue);
alertErrorWithFallback(intl, result.error, dmFailedMessage, displayValue);
}
} else {
result = await joinChannel(currentUserId, currentTeamId, channel.id);
if (result.error || !result.data || !result.data.channel) {
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.",
};
utils.alertErrorWithFallback(intl, result.error, joinFailedMessage, displayValue);
alertErrorWithFallback(intl, result.error, joinFailedMessage, displayValue);
}
}
@@ -226,22 +274,21 @@ export default class ChannelSidebar extends Component {
return;
}
this.selectChannel(result.data.channel || result.data, currentChannelId);
this.selectChannel(result.data, currentChannelId);
};
onPageSelected = (index) => {
this.swiperIndex = index;
if (this.swiperIndex === 0) {
this.setState({lockMode: 'locked-open'});
} else {
this.setState({lockMode: 'unlocked'});
}
};
onSearchEnds = () => {
//hack to update the drawer when the offset changes
const {isLandscape, isTablet} = this.props;
if (this.refs.drawer) {
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
}
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if (isLandscape || isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
@@ -250,6 +297,10 @@ export default class ChannelSidebar extends Component {
};
onSearchStart = () => {
if (this.refs.drawer) {
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
}
this.setState({openDrawerOffset: 0});
};
@@ -265,7 +316,7 @@ export default class ChannelSidebar extends Component {
}
};
renderNavigationView = () => {
renderContent = () => {
const {
navigator,
teamsCount,
@@ -273,14 +324,9 @@ export default class ChannelSidebar extends Component {
} = this.props;
const {
show,
openDrawerOffset,
} = this.state;
if (!show) {
return null;
}
const multipleTeams = teamsCount > 1;
const showTeams = openDrawerOffset !== 0 && multipleTeams;
if (this.drawerSwiper) {
@@ -343,20 +389,53 @@ export default class ChannelSidebar extends Component {
};
render() {
const {children, deviceWidth} = this.props;
const {lockMode, openDrawerOffset} = this.state;
const {children, isLandscape} = 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 (
<DrawerLayout
drawerLockMode={lockMode}
<Drawer
ref='drawer'
renderNavigationView={this.renderNavigationView}
onDrawerClose={this.handleDrawerClose}
onDrawerOpen={this.handleDrawerOpen}
drawerWidth={deviceWidth - openDrawerOffset}
onOpenStart={this.handleDrawerOpenStart}
onOpen={this.handleDrawerOpen}
onClose={this.handleDrawerClose}
onCloseStart={this.handleDrawerCloseStart}
captureGestures='open'
type='static'
acceptTap={true}
acceptPanOnDrawer={false}
disabled={false}
content={this.renderContent()}
tapToClose={true}
openDrawerOffset={openDrawerOffset}
onRequestClose={this.closeChannelDrawer}
panOpenMask={0.2}
panCloseMask={openDrawerOffset}
panThreshold={0.25}
acceptPan={true}
negotiatePan={true}
useInteractionManager={false}
tweenDuration={100}
tweenHandler={this.handleDrawerTween}
elevation={-5}
bottomPanOffset={Platform.OS === 'ios' ? ANDROID_TOP_LANDSCAPE : IOS_TOP_PORTRAIT}
topPanOffset={Platform.OS === 'ios' ? iosTop : androidTop}
styles={{
main: {
shadowColor: '#000000',
shadowOpacity: 0.4,
shadowRadius: 12,
shadowOffset: {
width: -4,
height: 0,
},
},
}}
>
{children}
</DrawerLayout>
</Drawer>
);
}
}

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';
@@ -25,23 +25,15 @@ export default class ChannelItem extends PureComponent {
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,
showUnreadForMsgs: PropTypes.bool.isRequired,
status: PropTypes.string,
teammateDeletedAt: PropTypes.number,
type: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
unreadMsgs: PropTypes.number.isRequired,
};
static defaultProps = {
mentions: 0,
};
static contextTypes = {
@@ -79,30 +71,20 @@ export default class ChannelItem extends PureComponent {
this.previewRef = ref;
};
showChannelAsUnread = () => {
return this.props.mentions > 0 || (this.props.unreadMsgs > 0 && this.props.showUnreadForMsgs);
};
render() {
const {
channelId,
currentChannelId,
displayName,
isChannelMuted,
isMyUser,
isUnread,
mentions,
shouldHideChannel,
status,
teammateDeletedAt,
theme,
type,
} = this.props;
if (!this.showChannelAsUnread() && shouldHideChannel) {
return null;
}
const {intl} = this.context;
let channelDisplayName = displayName;
@@ -119,7 +101,6 @@ export default class ChannelItem extends PureComponent {
let extraItemStyle;
let extraTextStyle;
let extraBorder;
let mutedStyle;
if (isActive) {
extraItemStyle = style.itemActive;
@@ -146,10 +127,6 @@ export default class ChannelItem extends PureComponent {
);
}
if (isChannelMuted) {
mutedStyle = style.muted;
}
const icon = (
<ChannelIcon
isActive={isActive}
@@ -171,7 +148,7 @@ export default class ChannelItem extends PureComponent {
onPress={this.onPress}
onLongPress={this.onPreview}
>
<View style={[style.container, mutedStyle]}>
<View style={style.container}>
{extraBorder}
<View style={[style.item, extraItemStyle]}>
{icon}
@@ -204,6 +181,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
item: {
alignItems: 'center',
height: 44,
flex: 1,
flexDirection: 'row',
paddingLeft: 16,
@@ -214,13 +192,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
text: {
color: changeOpacity(theme.sidebarText, 0.4),
flex: 1,
fontSize: 14,
fontWeight: '600',
lineHeight: 16,
paddingRight: 40,
height: '100%',
flex: 1,
textAlignVertical: 'center',
lineHeight: 44,
},
textActive: {
color: theme.sidebarTextActiveColor,
@@ -229,7 +205,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
color: theme.sidebarUnreadText,
},
badge: {
backgroundColor: theme.mentionBg,
backgroundColor: theme.mentionBj,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
@@ -241,8 +217,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
color: theme.mentionColor,
fontSize: 10,
},
muted: {
opacity: 0.5,
},
};
});

View File

@@ -0,0 +1,48 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {General} from 'mattermost-redux/constants';
import {getCurrentChannelId, makeGetChannel, getMyChannelMember} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users';
import ChannelItem from './channel_item';
function makeMapStateToProps() {
const getChannel = makeGetChannel();
return (state, ownProps) => {
const channel = ownProps.channel || getChannel(state, {id: ownProps.channelId});
let member;
if (ownProps.isUnread) {
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;
}
}
return {
currentChannelId: getCurrentChannelId(state),
displayName: channel.display_name,
fake: channel.fake,
isMyUser,
mentions: member ? member.mention_count : 0,
status: channel.status,
teammateDeletedAt,
theme: getTheme(state),
type: channel.type,
};
};
}
export default connect(makeMapStateToProps)(ChannelItem);

View File

@@ -1,27 +1,28 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import {
Platform,
View,
} from 'react-native';
import {intlShape} from 'react-intl';
import {injectIntl, intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import SearchBar from 'app/components/search_bar';
import {ViewTypes} from 'app/constants';
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;
let FilteredList = null;
export default class ChannelsList extends PureComponent {
class ChannelsList extends React.PureComponent {
static propTypes = {
intl: intlShape.isRequired,
navigator: PropTypes.object,
onJoinChannel: PropTypes.func.isRequired,
onSearchEnds: PropTypes.func.isRequired,
@@ -31,10 +32,6 @@ export default class ChannelsList extends PureComponent {
theme: PropTypes.object.isRequired,
};
static contextTypes = {
intl: intlShape.isRequired,
};
constructor(props) {
super(props);
@@ -49,15 +46,15 @@ export default class ChannelsList extends PureComponent {
}
onSelectChannel = (channel, currentChannelId) => {
if (this.refs.search_bar) {
this.refs.search_bar.cancel();
}
if (channel.fake) {
this.props.onJoinChannel(channel, currentChannelId);
} else {
this.props.onSelectChannel(channel, currentChannelId);
}
if (this.refs.search_bar) {
this.refs.search_bar.cancel();
}
};
onSearch = (term) => {
@@ -65,9 +62,6 @@ export default class ChannelsList extends PureComponent {
};
onSearchFocused = () => {
if (!FilteredList) {
FilteredList = require('./filtered_list').default;
}
this.setState({searching: true});
this.props.onSearchStart();
};
@@ -79,8 +73,8 @@ export default class ChannelsList extends PureComponent {
};
render() {
const {intl} = this.context;
const {
intl,
navigator,
onShowTeams,
theme,
@@ -244,8 +238,10 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
lineHeight: 18,
},
above: {
backgroundColor: theme.mentionBg,
backgroundColor: theme.mentionBj,
top: 9,
},
};
});
export default injectIntl(ChannelsList);

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import deepEqual from 'deep-equal';
import React, {Component} from 'react';
@@ -19,7 +19,7 @@ import {General} from 'mattermost-redux/constants';
import {sortChannelsByDisplayName} from 'mattermost-redux/utils/channel_utils';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import ChannelItem from 'app/components/sidebars/main/channels_list/channel_item';
import ChannelDrawerItem from 'app/components/channel_drawer/channels_list/channel_item';
import {ListTypes} from 'app/constants';
const VIEWABILITY_CONFIG = ListTypes.VISIBILITY_CONFIG_DEFAULTS;
@@ -109,12 +109,11 @@ class FilteredList extends Component {
createChannelElement = (channel) => {
return (
<ChannelItem
<ChannelDrawerItem
ref={channel.id}
channelId={channel.id}
channel={channel}
isSearchResult={true}
isUnread={channel.isUnread}
isUnread={false}
mentions={0}
onSelectChannel={this.onSelectChannel}
/>
@@ -173,11 +172,8 @@ class FilteredList extends Component {
buildUnreadChannelsForSearch = (props, term) => {
const {unreadChannels} = props.channels;
return this.filterChannels(unreadChannels, term).map((item) => {
item.isUnread = true;
return item;
});
};
return this.filterChannels(unreadChannels, term);
}
buildCurrentDMSForSearch = (props, term) => {
const {channels, teammateNameDisplay, profiles, statuses, pastDirectMessages, groupChannelMemberDetails} = props;

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
@@ -12,30 +12,23 @@ import {
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 {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
import List from './list';
function mapStateToProps(state) {
const config = getConfig(state);
const license = getLicense(state);
const {config, license} = state.entities.general;
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,

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';
@@ -16,7 +16,8 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {General} from 'mattermost-redux/constants';
import {debounce} from 'mattermost-redux/actions/helpers';
import ChannelItem from 'app/components/sidebars/main/channels_list/channel_item';
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 {changeOpacity} from 'app/utils/theme';
@@ -26,8 +27,6 @@ const VIEWABILITY_CONFIG = {
waitForInteraction: true,
};
let UnreadIndicator = null;
export default class List extends PureComponent {
static propTypes = {
canCreatePrivateChannels: PropTypes.bool.isRequired,
@@ -258,7 +257,6 @@ export default class List extends PureComponent {
return (
<ChannelItem
channelId={item}
isFavorite={this.props.favoriteChannelIds.includes(item)}
navigator={this.props.navigator}
onSelectChannel={this.onSelectChannel}
/>
@@ -312,9 +310,6 @@ export default class List extends PureComponent {
};
emitUnreadIndicatorChange = debounce((showIndicator) => {
if (showIndicator && !UnreadIndicator) {
UnreadIndicator = require('app/components/sidebars/main/channels_list/unread_indicator').default;
}
this.setState({showIndicator});
}, 100);
@@ -353,14 +348,12 @@ export default class List extends PureComponent {
stickySectionHeadersEnabled={false}
viewabilityConfig={VIEWABILITY_CONFIG}
/>
{showIndicator &&
<UnreadIndicator
show={showIndicator}
style={[styles.above, {width}]}
onPress={this.scrollToTop}
theme={theme}
/>
}
</View>
);
}

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
@@ -116,7 +116,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
fontSize: 14,
},
badge: {
backgroundColor: theme.mentionBg,
backgroundColor: theme.mentionBj,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';
@@ -25,11 +25,9 @@ export default class DrawerSwiper extends Component {
};
shouldComponentUpdate(nextProps) {
const {deviceWidth, openDrawerOffset, showTeams, theme} = this.props;
const {deviceWidth, showTeams, theme} = this.props;
return nextProps.deviceWidth !== deviceWidth ||
nextProps.showTeams !== showTeams ||
nextProps.openDrawerOffset !== openDrawerOffset ||
nextProps.theme !== theme;
nextProps.showTeams !== showTeams || nextProps.theme !== theme;
}
runOnLayout = (shouldRun = true) => {

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';

View File

@@ -1,25 +1,24 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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 {joinChannel} from 'mattermost-redux/actions/channels';
import {joinChannel, markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
import {getTeams} from 'mattermost-redux/actions/teams';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId, getMyTeamsCount} from 'mattermost-redux/selectors/entities/teams';
import {setChannelDisplayName, setChannelLoading} from 'app/actions/views/channel';
import {handleSelectChannel, setChannelDisplayName, setChannelLoading} from 'app/actions/views/channel';
import {makeDirectChannel} from 'app/actions/views/more_dms';
import {isLandscape, isTablet, getDimensions} from 'app/selectors/device';
import {isLandscape, isTablet} from 'app/selectors/device';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import MainSidebar from './main_sidebar.js';
import ChannelDrawer from './channel_drawer.js';
function mapStateToProps(state) {
const {currentUserId} = state.entities.users;
return {
...getDimensions(state),
currentTeamId: getCurrentTeamId(state),
currentUserId,
isLandscape: isLandscape(state),
@@ -33,12 +32,15 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getTeams,
handleSelectChannel,
joinChannel,
markChannelAsViewed,
makeDirectChannel,
markChannelAsRead,
setChannelDisplayName,
setChannelLoading,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps, null, {withRef: true})(MainSidebar);
export default connect(mapStateToProps, mapDispatchToProps, null, {withRef: true})(ChannelDrawer);

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// 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';
@@ -11,7 +11,7 @@ import {
TouchableHighlight,
View,
} from 'react-native';
import {intlShape} from 'react-intl';
import {injectIntl, intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FormattedText from 'app/components/formatted_text';
@@ -28,7 +28,7 @@ const VIEWABILITY_CONFIG = {
waitForInteraction: true,
};
export default class TeamsList extends PureComponent {
class TeamsList extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
handleTeamChange: PropTypes.func.isRequired,
@@ -36,15 +36,12 @@ export default class TeamsList extends PureComponent {
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,
};
static contextTypes = {
intl: intlShape.isRequired,
};
constructor(props) {
super(props);
@@ -67,8 +64,7 @@ export default class TeamsList extends PureComponent {
};
goToSelectTeam = preventDoubleTap(() => {
const {intl} = this.context;
const {currentUrl, navigator, theme} = this.props;
const {currentUrl, intl, navigator, theme} = this.props;
navigator.showModal({
screen: 'SelectTeam',
@@ -199,3 +195,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
};
});
export default injectIntl(TeamsList);

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';

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