Compare commits

..

8 Commits

Author SHA1 Message Date
Elias Nahum
78aba1fcb6 Bump Android build number to 122 (#1883) 2018-07-04 10:50:39 -04:00
Elias Nahum
cba4908854 Bump iOS build number to 122 (#1882) 2018-07-04 10:41:47 -04:00
Harrison Healey
9169f6f694 Add setNativeProps to QuickTextInput (#1880) 2018-07-04 09:38:00 -04:00
Elias Nahum
bdc487ab20 Bump Android build number to 121 (#1875) 2018-07-03 19:44:54 -04:00
Elias Nahum
d7cc95c7f8 Bump iOS build number to 121 (#1874) 2018-07-03 19:39:18 -04:00
Elias Nahum
52ad889bfc Bump Android build number to 120 (#1869) 2018-07-03 19:20:07 -04:00
Elias Nahum
7be6911b2c Bump iOS build number to 120 (#1868) 2018-07-03 19:19:02 -04:00
Harrison Healey
509dfe5542 MM-11116 Re-added QuickTextInput and added another hack on top of it (#1871)
* MM-11116 Re-added updated QuickTextInput component to work around RN issue

* MM-11116 Work around setNativeProps not working for TextInputs

* Add isFocused method to QuickTextInput
2018-07-03 19:04:04 -04:00
716 changed files with 63028 additions and 31788 deletions

16
.babelrc Normal file
View File

@@ -0,0 +1,16 @@
{
"presets": [ "react-native" ],
"env": {
"production": {
"plugins": ["transform-remove-console"]
}
},
"plugins": [
["module-resolver", {
"root": ["./src", "."],
"alias": {
"assets": "./dist/assets"
}
}]
]
}

View File

@@ -1,8 +1,5 @@
{
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
@@ -13,15 +10,8 @@
}
},
"parser": "babel-eslint",
"settings": {
"react": {
"pragma": "React",
"version": "16.5"
}
},
"plugins": [
"react",
"header"
"react"
],
"env": {
"browser": true,
@@ -41,8 +31,7 @@
"expect": true,
"it": true,
"jest": true,
"test": true,
"__DEV__": true
"test": true
},
"rules": {
"array-bracket-spacing": [2, "never"],
@@ -57,7 +46,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"],
@@ -72,7 +61,6 @@
"generator-star-spacing": [0, {"before": false, "after": true}],
"global-require": 0,
"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"],
@@ -229,7 +217,7 @@
"react/jsx-uses-vars": 2,
"react/jsx-no-comment-textnodes": 2,
"react/no-danger": 0,
"react/no-deprecated": 1,
"react/no-deprecated": 2,
"react/no-did-mount-set-state": 2,
"react/no-did-update-set-state": 2,
"react/no-direct-mutation-state": 2,

View File

@@ -16,55 +16,33 @@
; Ignore polyfills
.*/Libraries/polyfills/.*
; Ignore metro
.*/node_modules/metro/.*
[include]
[libs]
node_modules/react-native/Libraries/react-native/react-native-interface.js
node_modules/react-native/flow/
node_modules/react-native/flow-github/
[options]
emoji=true
esproposal.optional_chaining=enable
esproposal.nullish_coalescing=enable
module.system=haste
module.system.haste.use_name_reducers=true
# get basename
module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1'
# strip .js or .js.flow suffix
module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1'
# strip .ios suffix
module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1'
module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1'
module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1'
module.system.haste.paths.blacklist=.*/__tests__/.*
module.system.haste.paths.blacklist=.*/__mocks__/.*
module.system.haste.paths.blacklist=<PROJECT_ROOT>/node_modules/react-native/Libraries/Animated/src/polyfills/.*
module.system.haste.paths.whitelist=<PROJECT_ROOT>/node_modules/react-native/Libraries/.*
munge_underscores=true
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
module.file_ext=.js
module.file_ext=.jsx
module.file_ext=.json
module.file_ext=.native.js
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState
suppress_type=$FixMe
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
unsafe.enable_getters_and_setters=true
[version]
^0.78.0
^0.56.0

11
.gitignore vendored
View File

@@ -20,7 +20,6 @@ build/
*.perspectivev3
!default.perspectivev3
xcuserdata
xcshareddata
*.xccheckout
*.moved-aside
DerivedData
@@ -33,6 +32,7 @@ xcshareddata/
# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
@@ -68,11 +68,10 @@ tags
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/
*/fastlane/report.xml
*/fastlane/Preview.html
fastlane/report.xml
fastlane/Preview.html
*/fastlane/screenshots
fastlane/.env
fastlane/report.xml
# Sentry
android/sentry.properties
@@ -85,8 +84,6 @@ ios/sentry.properties
.podinstall
ios/Pods/
# Bundle artifact
*.jsbundle
#editor-settings
.vscode

View File

@@ -1,177 +1,5 @@
# Mattermost Mobile Apps Changelog
## v1.12.0 Release
- Release Date: September 16, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Search Date Filters
- Search for messages before, on, or after a specified date.
### Improvements
- Added notification support for Android O and P.
### Bug Fixes
- Fixed an issue where Okta was not able to login in some deployments.
- Fixed an issue where messages in Direct Message channels did not show when clicking "Jump To".
- Fixed an issue where `Show More` on a post with a message attachment displayed a blank where content should have been.
- Prevent downloading of files when disallowed in the System Console.
- Fixed an issue where users could not click on attachment filenames to open them.
- Fixed an issue where email notification settings did not save from mobile.
- Fixed an issue where the share extension allowed users to select and attempt to share content to channels that had been archived.
- Fixed an issue where reacting to an existing emoji in an archived channel was allowed.
- Fixed an issue where archived channels sometimes remained in the drawer.
- Fixed an issue where deactivated users were not marked as such in Direct Message search.
## v1.11.0 Release
- Release Date: August 16, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Searching Archived Channels
- Added ability to search for archived channels. Requires Mattermost server v5.2 or later.
#### Deep Linking
- Added the ability for custom builds to open Mattermost links directly in the app rather than the default mobile browser. Learn more in our [documentation](https://docs.mattermost.com/mobile/mobile-faq.html#how-do-i-configure-deep-linking)
### Improvements
- Added profile pop-up to combined system messages.
- Force re-entering SSO auth credentials after logout.
- Added consecutive posts by the same user.
- Added a loading indicator when user info is still loading in the left-hand side.
### Bug Fixes
- Fixed an issue where Android devices showed an incorrect timestamp.
- Fixed an issue on Android where the app did not get sent to the background when pressing the hardware back button in the channel screen.
- Fixed an issue with video playback when the filename had spaces.
- Fixed an issue where the app crashed when playing YouTube videos.
- Fixed an issue with session expiration notification.
- Fixed an issue with sharing files from Google Drive in Android Share Extension.
- Fixed an issue on Android where replying to a push notification sometimes went to the wrong channel.
- Fixed an issue where the previous server URL was present on the input textbox before changing the screen to Login.
- Fixed an issue where user menu was not translated correctly.
- Fixed an issue where some field lengths in Account Settings didn't match the desktop app.
- Fixed an issue where long URLs for embedded images in message attachments got cut off and didn't render.
- Fixed an issue where link preview images were not cropped properly.
- Fixed an issue where long usernames didn't wrap properly in the Account Settings menu.
- Fixed an issue where DMs would not open if users were using "Jump To".
- Fixed an issue where no message was displayed after removing a user from a channel with join/leave messages disabled.
## v1.10.0 Release
- Release Date: July 16, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Channel drawer performance
- Android devices will notice significant performance improvements when opening and closing the channel drawer.
#### Channel loading performance
- Improved channel loading performance as post are retrieved with every push notification
#### Announcement banner improvements
- Markdown now renders when announcement banners are expanded
- When enabled by the System Admin, users can now dismiss announcement banners until their next session
### Improvements
- Combined consecutive messages from the same user.
- Added experimental support for certificate-based authentication (CBA) for iOS to identify a user or a device before granting access to Mattermost. See [documentation](https://docs.mattermost.com/deployment/certificate-based-authentication.html) to learn more.
- Added support for the experimental automatic direct message replies feature.
- Added support for the experimental timezone feature.
- Changed post textbox to not be a connected component.
- Allow connecting to mattermost instances hosted at subpaths.
- Added support for starting YouTube videos at a given time.
- Added support for keeping messages if slash command fails.
### Bug Fixes
- Fixed an issue where the unread badge background was always white.
- Fixed an issue where a username repeated in system message if user was added to a channel more than once.
- Fixed an issue where Android Sharing from Microsoft apps failed.
- Fixed an issue where YouTube crashed the app if link did not have a time set.
- Fixed an issue where System Admins did not see all teams available to join on mobile.
- Fixed an issue where users were unable to share from Files app.
- Fixed an issue where viewing a non-existent permalink didn't show an error message.
- Fixed an issue where jumping to a channel search did not bold unread channels.
- Fixed an issue with being able to add own user to a Group Message channel.
- Fixed an issue with not being able to reply from a push notification on iOS.
- Fixed an issue where the app did not display Brazilian language.
## 1.9.3 Release
- Release Date: July 04, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed multiple issues causing app crashes
- Fixed an issue on iOS devices with typing non-english characters in the post input box
## 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

View File

@@ -1,6 +1,6 @@
# Code Contribution Guidelines
Thank you for your interest in contributing! Please see the [Mattermost Contribution Guide](https://developers.mattermost.com/contribute/getting-started/) 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.
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.
### Review Process for this Repo

View File

@@ -1,4 +1,4 @@
Copyright 2015-present Mattermost, Inc.
Copyright 2016 Mattermost, Inc.
Apache License
Version 2.0, January 2004

View File

@@ -1,9 +1,8 @@
.PHONY: pre-run pre-build clean
.PHONY: pre-run clean
.PHONY: check-style
.PHONY: start stop
.PHONY: run run-ios run-android
.PHONY: build build-ios build-android unsigned-ios unsigned-android
.PHONY: build-pr can-build-pr prepare-pr
.PHONY: build-ios build-android unsigned-ios unsigned-android
.PHONY: test help
POD := $(shell which pod 2> /dev/null)
@@ -11,7 +10,7 @@ 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)
node_modules: package.json
.npminstall: package.json
@if ! [ $(shell which npm 2> /dev/null) ]; then \
echo "npm is not installed https://npmjs.com"; \
exit 1; \
@@ -20,14 +19,7 @@ node_modules: package.json
@echo Getting Javascript dependencies
@npm install
npm-ci: package.json
@if ! [ $(shell which npm 2> /dev/null) ]; then \
echo "npm is not installed https://npmjs.com"; \
exit 1; \
fi
@echo Getting Javascript dependencies
@npm ci
@touch $@
.podinstall:
ifeq ($(OS), Darwin)
@@ -51,11 +43,9 @@ dist/assets: $(BASE_ASSETS) $(OVERRIDE_ASSETS)
@echo "Generating app assets"
@node scripts/make-dist-assets.js
pre-run: | node_modules .podinstall dist/assets ## Installs dependencies and assets
pre-run: | .npminstall .podinstall dist/assets ## Installs dependencies and assets
pre-build: | npm-ci .podinstall dist/assets ## Install dependencies and assets before building
check-style: node_modules ## Runs eslint
check-style: .npminstall ## Runs eslint
@echo Checking for style guide compliance
@npm run check
@@ -63,6 +53,7 @@ clean: ## Cleans dependencies, previous builds and temp files
@echo Cleaning started
@rm -rf node_modules
@rm -f .npminstall
@rm -f .podinstall
@rm -rf dist
@rm -rf ios/build
@@ -72,6 +63,11 @@ clean: ## Cleans dependencies, previous builds and temp files
@echo Cleanup finished
post-install:
@./node_modules/.bin/remotedev-debugger --hostname localhost --port 5678 --injectserver
@# 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
@@ -88,9 +84,10 @@ post-install:
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
start: | pre-run ## Starts the React Native packager server
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start; \
else \
@@ -142,7 +139,7 @@ prepare-android-build:
run: run-ios ## alias for run-ios
run-ios: | check-device-ios pre-run ## Runs the app on an iOS simulator
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
@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 \
@@ -161,7 +158,7 @@ run-ios: | check-device-ios pre-run ## Runs the app on an iOS 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 -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
@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; \
if [ ! -z ${VARIANT} ]; then \
@@ -179,36 +176,26 @@ run-android: | check-device-android pre-run prepare-android-build ## Runs the ap
fi; \
fi
build: | stop pre-build check-style ## Builds the app for Android & iOS
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
fi
@echo "Building App"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
build-ios: | stop pre-build check-style ## Builds the iOS app
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
build-ios: | pre-run check-style ## Creates an iOS 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; \
fi
@echo "Building iOS app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios build
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
build-android: | stop pre-build check-style prepare-android-build ## Build the Android app
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
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; \
fi
@echo "Building Android app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android build
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
unsigned-ios: stop pre-build check-style ## Build an unsigned version of the iOS app
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
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; \
fi
@@ -219,36 +206,21 @@ unsigned-ios: stop pre-build check-style ## Build an unsigned version of the iOS
@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/
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
unsigned-android: stop pre-build check-style prepare-android-build ## Build an unsigned version of the Android app
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
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; \
fi
@echo "Building unsigned Android app"
@cd fastlane && NODE_ENV=production bundle exec fastlane android unsigned
@mv android/app/build/outputs/apk/unsigned/app-unsigned-unsigned.apk ./Mattermost-unsigned.apk
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
@mv android/app/build/outputs/apk/app-unsigned-unsigned.apk ./Mattermost-unsigned.apk
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
test: | pre-run check-style ## Runs tests
@npm test
build-pr: | can-build-pr stop pre-build check-style ## Build a PR from the mattermost-mobile repo
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
fi
@echo "Building App from PR ${PR_ID}"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build_pr pr:PR-${PR_ID}
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
can-build-pr:
@if [ -z ${PR_ID} ]; then \
echo a PR number needs to be specified; \
exit 1; \
fi
## Help documentation https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

3524
NOTICE.txt

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,9 @@
# Mattermost Mobile
- **Supported Server versions:** 4.0+
- **Supported iOS versions:** 9.3+
- **Supported Android versions:** 5.0+
**Supported Server Versions:** 4.0+
**Supported iOS versions:** 9.3+
**Supported Android versions:** 5.0+
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 14 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
@@ -10,8 +11,6 @@ You can download our apps from the [App Store](https://about.mattermost.com/matt
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

View File

@@ -45,13 +45,13 @@ android_library(
android_build_config(
name = "build_config",
package = "com.mattermost.rnbeta",
package = "com.mattermost-mobile",
)
android_resource(
name = "res",
package = "com.mattermost.rnbeta",
res = "src/main/res",
name = "res",
res = "src/main/res",
package = "com.mattermost.rnbeta",
)
android_binary(

View File

@@ -74,8 +74,8 @@ import com.android.build.OutputFile
project.ext.react = [
entryFile: "index.js",
bundleCommand: "ram-bundle",
bundleConfig: "packager-config.js"
bundleCommand: "unbundle",
bundleConfig: "packager/config.js"
]
apply from: "../../node_modules/react-native/react.gradle"
@@ -106,16 +106,15 @@ 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 rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 164
versionName "1.14.0"
multiDexEnabled = true
minSdkVersion 21
targetSdkVersion 23
versionCode 122
versionName "1.9.3"
ndk {
abiFilters "armeabi-v7a", "x86"
}
@@ -152,7 +151,6 @@ android {
unsigned.initWith(buildTypes.release)
unsigned {
signingConfig null
matchingFallbacks = ['debug', 'release']
}
}
// applicationVariants are e.g. debug, release
@@ -180,45 +178,46 @@ configurations.all {
resolutionStrategy {
eachDependency { DependencyResolveDetails details ->
if (details.requested.name == 'android-jsc') {
details.useTarget group: details.requested.group, name: 'android-jsc-intl', version: 'r224109'
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:${rootProject.ext.supportLibVersion}"
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:percent:27.1.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 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(':rn-fetch-blob')
implementation project(':react-native-recyclerview-list')
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-document-picker')
compile project(':react-native-keychain')
compile project(':react-native-doc-viewer')
compile project(':react-native-video')
compile project(':react-native-navigation')
compile project(':react-native-image-picker')
compile project(':react-native-bottom-sheet')
compile ('com.google.android.gms:play-services-gcm:9.4.0') {
force = true;
}
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:fresco:1.10.0'
implementation 'com.facebook.fresco:animated-gif:1.10.0'
compile 'com.facebook.fresco:animated-base-support:1.3.0'
// For WebP support, including animated WebP
implementation 'com.facebook.fresco:animated-webp:1.10.0'
implementation 'com.facebook.fresco:webpsupport:1.10.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

@@ -15,3 +15,56 @@
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Disabling obfuscation is useful if you collect stack traces from production crashes
# (unless you are using a system that supports de-obfuscate the stack traces).
-dontobfuscate
# React Native
# Keep our interfaces so they can be used by other ProGuard rules.
# See http://sourceforge.net/p/proguard/bugs/466/
-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip
# Do not strip any method/class that is annotated with @DoNotStrip
-keep @com.facebook.proguard.annotations.DoNotStrip class *
-keep @com.facebook.common.internal.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.proguard.annotations.DoNotStrip *;
@com.facebook.common.internal.DoNotStrip *;
}
-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
void set*(***);
*** get*();
}
-keep class * extends com.facebook.react.bridge.JavaScriptModule { *; }
-keep class * extends com.facebook.react.bridge.NativeModule { *; }
-keepclassmembers,includedescriptorclasses class * { native <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.UIProp <fields>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup <methods>; }
-dontwarn com.facebook.react.**
# TextLayoutBuilder uses a non-public Android constructor within StaticLayout.
# See libs/proxy/src/main/java/com/facebook/fbui/textlayoutbuilder/proxy for details.
-dontwarn android.text.StaticLayout
# okhttp
-keepattributes Signature
-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
# okio
-keep class sun.misc.Unsafe { *; }
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**

View File

@@ -1,32 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mattermost.rnbeta">
package="com.mattermost.rnbeta"
android:versionCode="1"
android:versionName="1.0">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<permission
android:name="${applicationId}.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="com.google.android.c2dm.permission.SEND" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="22" />
<application
android:name=".MainApplication"
android:allowBackup="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config"
>
<meta-data android:name="android.content.APP_RESTRICTIONS"
android:resource="@xml/app_restrictions" />
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="184930218130\"/>
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="184930218130\0"/>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:windowSoftInputMode="adjustResize"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="adjustResize">
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View File

@@ -1,7 +1,6 @@
package com.mattermost.rnbeta;
import android.app.PendingIntent;
import android.app.NotificationChannel;
import android.content.Intent;
import android.content.Context;
import android.content.res.Resources;
@@ -141,19 +140,8 @@ public class CustomPushNotification extends PushNotification {
String packageName = mContext.getPackageName();
NotificationPreferences notificationPreferences = NotificationPreferences.getInstance(mContext);
String CHANNEL_ID = "channel_01";
String CHANNEL_NAME = "Mattermost notifications";
// First, get a builder initialized with defaults from the core class.
final Notification.Builder notification = new Notification.Builder(mContext);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH);
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(channel);
notification.setChannelId(CHANNEL_ID);
}
Bundle bundle = mNotificationProps.asBundle();
String version = bundle.getString("version");
@@ -283,13 +271,7 @@ public class CustomPushNotification extends PushNotification {
replyIntent.setAction(KEY_TEXT_REPLY);
replyIntent.putExtra(NOTIFICATION_ID, notificationId);
replyIntent.putExtra("pushNotification", bundle);
PendingIntent replyPendingIntent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
replyPendingIntent = PendingIntent.getForegroundService(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
} else {
replyPendingIntent = PendingIntent.getService(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
PendingIntent replyPendingIntent = PendingIntent.getService(mContext, 103, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
.setLabel("Reply")

View File

@@ -2,7 +2,6 @@ package com.mattermost.rnbeta;
import com.mattermost.share.SharePackage;
import android.app.Activity;
import android.support.annotation.NonNull;
import android.content.Context;
import android.os.Bundle;
@@ -18,7 +17,6 @@ import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerPackage;
import com.RNFetchBlob.RNFetchBlobPackage;
import com.gantix.JailMonkey.JailMonkeyPackage;
import io.tradle.react.LocalAuthPackage;
import com.github.godness84.RNRecyclerViewList.RNRecyclerviewListPackage;
import com.facebook.react.ReactPackage;
import com.facebook.soloader.SoLoader;
@@ -68,7 +66,7 @@ public class MainApplication extends NavigationApplication implements INotificat
new JailMonkeyPackage(),
new RNFetchBlobPackage(),
new MattermostPackage(this),
new RNSentryPackage(),
new RNSentryPackage(this),
new ReactNativeExceptionHandlerPackage(),
new ReactNativeYouTube(),
new ReactVideoPackage(),
@@ -76,8 +74,7 @@ public class MainApplication extends NavigationApplication implements INotificat
new ReactNativeDocumentPicker(),
new SharePackage(this),
new KeychainPackage(),
new InitializationPackage(this),
new RNRecyclerviewListPackage()
new InitializationPackage(this)
);
}
@@ -99,7 +96,7 @@ public class MainApplication extends NavigationApplication implements INotificat
}
@Override
public boolean clearHostOnActivityDestroy(Activity activity) {
public boolean clearHostOnActivityDestroy() {
// This solves the issue where the splash screen does not go away
// after the app is killed by the OS cause of memory or a long time in the background
return false;

View File

@@ -1,13 +1,9 @@
package com.mattermost.rnbeta;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Build;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;
@@ -22,28 +18,6 @@ import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationReplyService extends HeadlessJsTaskService {
private Context mContext;
@Override
public void onCreate() {
super.onCreate();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
Context mContext = this.getApplicationContext();
final Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
String CHANNEL_ID = "Reply job";
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW);
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
Notification notification =
new Notification.Builder(mContext, CHANNEL_ID)
.setContentTitle("Replying to message")
.setContentText(packageName)
.setSmallIcon(smallIconResId)
.build();
startForeground(1, notification);
}
}
@Override
protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
mContext = getApplicationContext();

View File

@@ -10,7 +10,6 @@ import android.content.IntentFilter;
import android.os.Bundle;
import android.util.Log;
import android.util.ArraySet;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.content.res.Configuration;
@@ -70,13 +69,9 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
activity.getWindow().setFlags(LayoutParams.FLAG_SECURE,
LayoutParams.FLAG_SECURE);
}
if (managedConfig != null && managedConfig.size() > 0 && activity != null) {
if (managedConfig!= null && managedConfig.size() > 0 && activity != null) {
activity.registerReceiver(restrictionsReceiver, restrictionsFilter);
}
if (activity != null) {
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
}
}
@Override

View File

@@ -6,7 +6,6 @@ import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.content.ContentUris;
import android.content.ContentResolver;
import android.os.Environment;
@@ -96,33 +95,15 @@ public class RealPathUtil {
public static String getPathFromSavingTempFile(Context context, final Uri uri) {
File tmpFile;
String fileName = null;
// Try and get the filename from the Uri
try {
Cursor returnCursor =
context.getContentResolver().query(uri, null, null, null, null);
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
returnCursor.moveToFirst();
fileName = returnCursor.getString(nameIndex);
} catch (Exception e) {
// just continue to get the filename with the last segment of the path
}
try {
if (fileName == null) {
fileName = uri.getLastPathSegment().toString().trim();
}
String fileName = uri.getLastPathSegment();
File cacheDir = new File(context.getCacheDir(), "mmShare");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
String mimeType = getMimeType(uri.getPath());
tmpFile = new File(cacheDir, fileName);
tmpFile.createNewFile();
tmpFile = File.createTempFile("tmp", fileName, cacheDir);
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 459 KiB

BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 585 KiB

BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 590 KiB

BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 468 KiB

BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 608 KiB

View File

@@ -1,3 +1,4 @@
<?xml version="1.0"?>
<resources>
<string name="app_name">Mattermost Beta</string>
<string name="inAppPinCode_title">in-App Pincode</string>

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config>
<trust-anchors>
<!-- Trust preinstalled CAs -->
<certificates src="system" />
<!-- Additionally trust user added CAs -->
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>

View File

@@ -1,42 +1,20 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
buildToolsVersion = "27.0.3"
minSdkVersion = 21
compileSdkVersion = 27
targetSdkVersion = 26
supportLibVersion = "27.1.1"
}
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.4'
classpath 'com.google.gms:google-services:3.2.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
// in the individual module build.gradle files
}
}
subprojects {
afterEvaluate {
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
}
}
}
}
allprojects {
repositories {
google()
mavenLocal()
jcenter()
maven {
@@ -45,13 +23,7 @@ allprojects {
}
maven {
// Local Maven repo containing AARs with JSC library built for Android
url "$rootDir/../node_modules/jsc-android/dist"
url "$rootDir/../node_modules/jsc-android/android"
}
}
}
task wrapper(type: Wrapper) {
gradleVersion = '4.4'
distributionUrl = distributionUrl.replace("bin", "all")
}

View File

@@ -11,12 +11,10 @@
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx2048M
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
#android.enableAapt2=false
#android.useDeprecatedNdk=true
android.useDeprecatedNdk=true

Binary file not shown.

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.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

110
android/gradlew vendored
View File

@@ -1,4 +1,4 @@
#!/usr/bin/env sh
#!/usr/bin/env bash
##############################################################################
##
@@ -6,6 +6,47 @@
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
@@ -20,49 +61,9 @@ while [ -h "$PRG" ] ; do
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -89,7 +90,7 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@@ -113,7 +114,6 @@ fi
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
@@ -154,19 +154,11 @@ if $cygwin ; then
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
APP_ARGS=$(save "$@")
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

14
android/gradlew.bat vendored
View File

@@ -8,14 +8,14 @@
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@@ -46,9 +46,10 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
@@ -59,6 +60,11 @@ set _SKIP=2
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line

View File

@@ -13,8 +13,8 @@ include ':react-native-sentry'
project(':react-native-sentry').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sentry/android')
include ':react-native-exception-handler'
project(':react-native-exception-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-exception-handler/android')
include ':rn-fetch-blob'
project(':rn-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/rn-fetch-blob/android')
include ':react-native-fetch-blob'
project(':react-native-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fetch-blob/android')
include ':jail-monkey'
project(':jail-monkey').projectDir = new File(rootProject.projectDir, '../node_modules/jail-monkey/android')
include ':react-native-local-auth'
@@ -39,5 +39,3 @@ include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':react-native-linear-gradient'
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
include ':react-native-recyclerview-list'
project(':react-native-recyclerview-list').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-recyclerview-list/android')

View File

@@ -1,19 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {networkStatusChangedAction} from 'redux-offline';
import {Client4} from 'mattermost-redux/client';
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {DeviceTypes} from 'app/constants';
export function connection(isOnline) {
return async (dispatch) => {
Client4.setOnline(isOnline);
dispatch(networkStatusChangedAction(isOnline));
return async (dispatch, getState) => {
dispatch({
type: DeviceTypes.CONNECTION_CHANGED,
data: isOnline,
});
}, getState);
};
}

View File

@@ -0,0 +1,35 @@
// 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';
export function handleUpdateUserNotifyProps(notifyProps) {
return async (dispatch, getState) => {
const state = getState();
const config = state.entities.general.config;
const {currentUserId} = state.entities.users;
const {interval, user_id: userId, ...otherProps} = notifyProps;
const email = notifyProps.email;
if (config.EnableEmailBatching === 'true' && email !== 'false') {
const emailInterval = [{
user_id: userId,
category: Preferences.CATEGORY_NOTIFICATIONS,
name: Preferences.EMAIL_INTERVAL,
value: interval,
}];
savePreferences(currentUserId, emailInterval)(dispatch, getState);
}
const props = {...otherProps, email};
try {
await updateMe({notify_props: props})(dispatch, getState);
} catch (error) {
// do nothing
}
};
}

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';
@@ -20,7 +20,6 @@ import {getProfilesInChannel} from 'mattermost-redux/actions/users';
import {General, Preferences} from 'mattermost-redux/constants';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getTeamByName} from 'mattermost-redux/selectors/entities/teams';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {
getChannelByName,
@@ -209,7 +208,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));
@@ -260,11 +259,8 @@ export function selectInitialChannel(teamId) {
const isGMVisible = lastChannel && lastChannel.type === General.GM_CHANNEL &&
isGroupChannelVisible(myPreferences, lastChannel);
if (
myMembers[lastChannelId] &&
lastChannel &&
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)
) {
if (lastChannelId && myMembers[lastChannelId] &&
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)) {
handleSelectChannel(lastChannelId)(dispatch, getState);
markChannelAsRead(lastChannelId)(dispatch, getState);
return;
@@ -274,40 +270,6 @@ export function selectInitialChannel(teamId) {
};
}
export function selectPenultimateChannel(teamId) {
return (dispatch, getState) => {
const state = getState();
const {channels, myMembers} = state.entities.channels;
const {currentUserId} = state.entities.users;
const {myPreferences} = state.entities.preferences;
const viewArchivedChannels = getConfig(state).ExperimentalViewArchivedChannels === 'true';
const lastChannelForTeam = state.views.team.lastChannelForTeam[teamId];
const lastChannelId = lastChannelForTeam && lastChannelForTeam.length > 1 ? lastChannelForTeam[1] : '';
const lastChannel = channels[lastChannelId];
const isDMVisible = lastChannel && lastChannel.type === General.DM_CHANNEL &&
isDirectChannelVisible(currentUserId, myPreferences, lastChannel);
const isGMVisible = lastChannel && lastChannel.type === General.GM_CHANNEL &&
isGroupChannelVisible(myPreferences, lastChannel);
if (
myMembers[lastChannelId] &&
lastChannel &&
(lastChannel.delete_at === 0 || viewArchivedChannels) &&
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)
) {
dispatch(setChannelLoading(true));
dispatch(setChannelDisplayName(lastChannel.display_name));
dispatch(handleSelectChannel(lastChannelId));
dispatch(markChannelAsRead(lastChannelId));
return;
}
dispatch(selectDefaultChannel(teamId));
};
}
export function selectDefaultChannel(teamId) {
return (dispatch, getState) => {
const channels = getState().entities.channels.channels;
@@ -549,19 +511,17 @@ 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;
return Boolean(posts);
};
}

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,9 +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 {ViewTypes} from 'app/constants';
import {uploadProfileImage, updateMe} from 'mattermost-redux/actions/users';
import {buildFileUploadData} from 'app/utils/file';
export function updateUser(user, success, error) {
return async (dispatch, getState) => {
@@ -18,14 +17,9 @@ export function updateUser(user, success, error) {
};
}
export function setProfileImageUri(imageUri = '') {
return {
type: ViewTypes.SET_PROFILE_IMAGE_URI,
imageUri,
export function handleUploadProfileImage(image, userId) {
return async (dispatch, getState) => {
const imageData = buildFileUploadData(image);
return await uploadProfileImage(userId, imageData)(dispatch, getState);
};
}
export default {
updateUser,
setProfileImageUri,
};

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

@@ -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 {FileTypes} from 'mattermost-redux/action_types';

View File

@@ -1,17 +1,13 @@
// 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 {getSessions} from 'mattermost-redux/actions/users';
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) => {
@@ -34,8 +30,7 @@ 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} = state.entities.general;
const token = Client4.getToken();
const url = Client4.getUrl();
const deviceToken = state.entities.general.deviceToken;
@@ -43,11 +38,6 @@ export function handleSuccessfulLogin() {
app.setAppCredentials(deviceToken, currentUserId, token, url);
const enableTimezone = isTimezoneEnabled(state);
if (enableTimezone) {
dispatch(autoUpdateTimezone(getDeviceTimezone()));
}
dispatch({
type: GeneralTypes.RECEIVED_APP_CREDENTIALS,
data: {
@@ -71,27 +61,18 @@ export function getSession() {
return async (dispatch, getState) => {
const state = getState();
const {currentUserId} = state.entities.users;
const {deviceToken} = state.entities.general;
const {credentials} = state.entities.general;
const token = credentials && credentials.token;
if (!currentUserId || !deviceToken) {
return 0;
if (currentUserId && token) {
const session = await Client4.getSessions(currentUserId, token);
if (Array.isArray(session) && session[0]) {
const s = session[0];
return s.expires_at;
}
}
let sessions;
try {
sessions = await dispatch(getSessions(currentUserId));
} catch (e) {
console.warn('Failed to get current session', e); // eslint-disable-line no-console
return 0;
}
if (!Array.isArray(sessions.data)) {
return 0;
}
const session = sessions.data.find((s) => s.device_id === deviceToken);
return session && session.expires_at ? session.expires_at : 0;
return false;
};
}

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// See License.txt for license information.
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
@@ -11,6 +11,13 @@ import {
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(),

View File

@@ -1,12 +1,12 @@
// 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;
@@ -23,11 +23,11 @@ export function makeDirectChannel(otherUserId, switchToChannel = true) {
dispatch(toggleDMChannel(otherUserId, 'true', channel.id));
} else {
result = await dispatch(createDirectChannel(currentUserId, otherUserId));
result = await createDirectChannel(currentUserId, otherUserId)(dispatch, getState);
channel = result.data;
}
if (channel && switchToChannel) {
if (channel) {
dispatch(handleSelectChannel(channel.id));
}

View File

@@ -1,11 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// See License.txt for license information.
import {Posts} from 'mattermost-redux/constants';
import {PostTypes} from 'mattermost-redux/action_types';
import {doPostAction} from 'mattermost-redux/actions/posts';
import {ViewTypes} from 'app/constants';
import {generateId} from 'app/utils/file';
@@ -40,31 +37,3 @@ export function sendAddToChannelEphemeralPost(user, addedUsername, message, chan
});
};
}
export function setMenuActionSelector(dataSource, onSelect, options) {
return {
type: ViewTypes.SELECTED_ACTION_MENU,
data: {
dataSource,
onSelect,
options,
},
};
}
export function selectAttachmentMenuAction(postId, actionId, displayText, value) {
return (dispatch) => {
dispatch({
type: ViewTypes.SUBMIT_ATTACHMENT_MENU_ACTION,
postId,
data: {
[actionId]: {
displayText,
value,
},
},
});
dispatch(doPostAction(postId, actionId, value));
};
}

View File

@@ -1,11 +1,12 @@
// 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 {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';
@@ -14,6 +15,7 @@ import {recordTime} from 'app/utils/segment';
import {
handleSelectChannel,
setChannelDisplayName,
retryGetPostsAction,
} from 'app/actions/views/channel';
export function startDataCleanup() {
@@ -55,15 +57,10 @@ export function loadFromPushNotification(notification) {
const {data} = notification;
const {currentTeamId, teams, myMembers: myTeamMembers} = state.entities.teams;
const {currentChannelId, channels} = state.entities.channels;
const channelId = data.channel_id;
let channelId = '';
let teamId = currentTeamId;
if (data) {
channelId = data.channel_id;
// when the notification does not have a team id is because its from a DM or GM
teamId = data.team_id || currentTeamId;
}
// 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 = [];
@@ -86,10 +83,12 @@ export function loadFromPushNotification(notification) {
dispatch(selectTeam({id: teamId}));
}
// 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) {
dispatch(markChannelAsRead(channelId, null, false));
await retryGetPostsAction(getPosts(channelId), dispatch, getState);
} else {
// when the notification is from a channel other than the current channel
dispatch(markChannelAsRead(channelId, currentChannelId, false));
dispatch(setChannelDisplayName(''));
@@ -102,10 +101,8 @@ export function purgeOfflineStore() {
return {type: General.OFFLINE_STORE_PURGE};
}
// A non-optimistic version of the createPost action in mattermost-redux with the file handling
// removed since it's not needed.
export function createPostForNotificationReply(post) {
return async (dispatch, getState) => {
export function createPost(post) {
return (dispatch, getState) => {
const state = getState();
const currentUserId = state.entities.users.currentUserId;
@@ -119,23 +116,18 @@ export function createPostForNotificationReply(post) {
update_at: timestamp,
};
try {
const data = await Client4.createPost({...newPost, create_at: 0});
return Client4.createPost({...newPost, create_at: 0}).then((payload) => {
dispatch({
type: PostTypes.RECEIVED_POSTS,
data: {
order: [],
posts: {
[data.id]: data,
[payload.id]: payload,
},
},
channelId: data.channel_id,
channelId: payload.channel_id,
});
return {data};
} catch (error) {
return {error};
}
});
};
}
@@ -147,13 +139,6 @@ export function recordLoadTime(screenName, category) {
};
}
export function setDeepLinkURL(url) {
return {
type: ViewTypes.SET_DEEP_LINK_URL,
url,
};
}
export default {
loadConfigAndLicense,
loadFromPushNotification,

View File

@@ -1,7 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';
@@ -13,25 +11,3 @@ export function handleSearchDraftChanged(text) {
}, getState);
};
}
export function showSearchModal(navigator, initialValue = '') {
return (dispatch, getState) => {
const theme = getTheme(getState());
const options = {
screen: 'Search',
animated: true,
backButtonTitle: '',
overrideBackPress: true,
passProps: {
initialValue,
},
navigatorStyle: {
navBarHidden: true,
screenBackgroundColor: theme.centerChannelBg,
},
};
navigator.showModal(options);
};
}

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,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// See License.txt for license information.
import {batchActions} from 'redux-batched-actions';
import configureStore from 'redux-mock-store';
@@ -11,6 +11,13 @@ 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', () => {

View File

@@ -1,18 +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 {batchActions} from 'redux-batched-actions';
import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types';
import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
import {getMyTeams} from 'mattermost-redux/actions/teams';
import {RequestStatus} from 'mattermost-redux/constants';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {NavigationTypes} from 'app/constants';
import {selectFirstAvailableTeam} from 'app/utils/teams';
import {setChannelDisplayName} from './channel';
@@ -44,34 +40,23 @@ export function selectDefaultTeam() {
return async (dispatch, getState) => {
const state = getState();
const {ExperimentalPrimaryTeam} = getConfig(state);
const {ExperimentalPrimaryTeam} = state.entities.general.config;
const {teams: allTeams, myMembers} = state.entities.teams;
const teams = Object.keys(myMembers).map((key) => allTeams[key]);
let defaultTeam = selectFirstAvailableTeam(teams, ExperimentalPrimaryTeam);
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) {
dispatch(handleTeamChange(defaultTeam.id));
} else if (state.requests.teams.getTeams.status === RequestStatus.FAILURE || state.requests.teams.getMyTeams.status === RequestStatus.FAILURE) {
EventEmitter.emit(NavigationTypes.NAVIGATION_ERROR_TEAMS);
handleTeamChange(defaultTeam.id)(dispatch, getState);
} else {
// If for some reason we reached this point cause of a failure in rehydration or something
// lets fetch the teams one more time to make sure the user does not belong to any team
const {data, error} = await dispatch(getMyTeams());
if (error) {
EventEmitter.emit(NavigationTypes.NAVIGATION_ERROR_TEAMS);
return;
}
if (data) {
defaultTeam = selectFirstAvailableTeam(data, ExperimentalPrimaryTeam);
}
if (defaultTeam) {
dispatch(handleTeamChange(defaultTeam.id));
} else {
EventEmitter.emit(NavigationTypes.NAVIGATION_NO_TEAMS);
}
EventEmitter.emit(NavigationTypes.NAVIGATION_NO_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 {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.
// See License.txt for license information.
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
@@ -11,6 +11,13 @@ import {
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', () => {

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,15 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// See License.txt for license information.
/* eslint-disable global-require*/
import {AsyncStorage, Linking, NativeModules, Platform, Text} from 'react-native';
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 {setDeepLinkURL} from 'app/actions/views/root';
import {ViewTypes} from 'app/constants';
import tracker from 'app/utils/time_tracker';
import {getCurrentLocale} from 'app/selectors/i18n';
@@ -51,48 +50,10 @@ export default class App {
this.token = null;
this.url = null;
// Load polyfill for iOS 9
if (Platform.OS === 'ios') {
const majorVersionIOS = parseInt(Platform.Version, 10);
if (majorVersionIOS < 10) {
require('@babel/polyfill');
}
}
// Usage deeplinking
Linking.addEventListener('url', this.handleDeepLink);
this.setFontFamily();
this.getStartupThemes();
this.getAppCredentials();
}
setFontFamily = () => {
// Set a global font for Android
if (Platform.OS === 'android') {
const defaultFontFamily = {
style: {
fontFamily: 'Roboto',
},
};
const TextRender = Text.render;
const initialDefaultProps = Text.defaultProps;
Text.defaultProps = {
...initialDefaultProps,
...defaultFontFamily,
};
Text.render = function render(props, ...args) {
const oldProps = props;
let newProps = {...props, style: [defaultFontFamily.style, props.style]};
try {
return Reflect.apply(TextRender, this, [newProps, ...args]);
} finally {
newProps = oldProps;
}
};
}
};
getTranslations = () => {
if (this.translations) {
return this.translations;
@@ -130,20 +91,13 @@ export default class App {
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;
}
this.deviceToken = deviceToken;
this.currentUserId = currentUserId;
this.token = token;
this.url = url;
Client4.setUrl(url);
Client4.setToken(token);
}
} else {
this.waitForRehydration = false;
}
} catch (error) {
return null;
@@ -198,7 +152,6 @@ export default class App {
if (!currentUserId) {
return;
}
const username = `${deviceToken}, ${currentUserId}`;
const password = `${token},${url}`;
@@ -208,14 +161,7 @@ export default class App {
this.url = url;
}
// Only save to keychain if the url and token are set
if (url && token) {
try {
setGenericPassword(username, password);
} catch (e) {
console.warn('could not set credentials', e); //eslint-disable-line no-console
}
}
setGenericPassword(username, password);
};
setStartupThemes = (toolbarBackground, toolbarTextColor, appBackground) => {
@@ -269,11 +215,6 @@ export default class App {
]);
};
handleDeepLink = (event) => {
const {url} = event;
store.dispatch(setDeepLinkURL(url));
}
launchApp = async () => {
const shouldStart = await handleManagedConfig();
if (shouldStart) {
@@ -288,21 +229,11 @@ export default class App {
const {dispatch} = store;
Linking.getInitialURL().then((url) => {
dispatch(setDeepLinkURL(url));
});
let screen = 'SelectServer';
if (this.token && this.url) {
screen = 'Channel';
tracker.initialLoad = Date.now();
try {
dispatch(loadMe());
} catch (e) {
// Fall through since we should have a previous version of the current user because we have a token
console.warn('Failed to load current user when starting on Channel screen', e); // eslint-disable-line no-console
}
dispatch(loadMe());
}
switch (screen) {

View File

@@ -1,62 +1,365 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AnnouncementBanner should match snapshot 1`] = `
<AnimatedComponent
style={
Array [
ShallowWrapper {
"length": 1,
Symbol(enzyme.__root__): [Circular],
Symbol(enzyme.__unrendered__): <AnnouncementBanner
actions={
Object {
"overflow": "hidden",
"paddingHorizontal": 10,
"position": "absolute",
"top": 0,
"width": "100%",
},
Object {
"backgroundColor": "#ddd",
"height": 0,
},
]
}
>
<TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"dismissBanner": [MockFunction],
}
}
>
<Component
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
allowDismissal={true}
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,
"fontSize": 14,
"marginRight": 5,
"flexDirection": "row",
}
}
>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"flex": 1,
"fontSize": 14,
"marginRight": 5,
},
Object {
"color": "#fff",
},
]
}
>
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",
},
]
}
>
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": "Banner Text",
"ellipsizeMode": "tail",
"numberOfLines": 1,
"style": Array [
Object {
"flex": 1,
"fontSize": 14,
"marginRight": 5,
},
Object {
"color": "#fff",
},
],
},
"ref": null,
"rendered": "Banner Text",
"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",
},
]
}
>
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 {
"color": "#fff",
"backgroundColor": "#ddd",
"height": 0,
},
]
}
>
<RemoveMarkdown
value="Banner Text"
/>
</Component>
<Icon
allowFontScaling={false}
color="#fff"
name="info"
size={16}
/>
</TouchableOpacity>
</AnimatedComponent>
],
},
"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",
},
]
}
>
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": "Banner Text",
"ellipsizeMode": "tail",
"numberOfLines": 1,
"style": Array [
Object {
"flex": 1,
"fontSize": 14,
"marginRight": 5,
},
Object {
"color": "#fff",
},
],
},
"ref": null,
"rendered": "Banner Text",
"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`] = `null`;
exports[`AnnouncementBanner should match snapshot 2`] = `
ShallowWrapper {
"length": 1,
Symbol(enzyme.__root__): [Circular],
Symbol(enzyme.__unrendered__): <AnnouncementBanner
actions={
Object {
"dismissBanner": [MockFunction],
}
}
allowDismissal={true}
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 = {
@@ -51,24 +52,30 @@ export default class AnnouncementBanner extends PureComponent {
}
}
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) => {
@@ -113,7 +120,7 @@ export default class AnnouncementBanner extends PureComponent {
numberOfLines={1}
style={[style.bannerText, bannerTextStyle]}
>
<RemoveMarkdown value={bannerText}/>
{bannerText}
</Text>
<MaterialIcons
color={bannerTextColor}

View File

@@ -1,10 +1,10 @@
// 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 {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
import {configure, shallow} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({adapter: new Adapter()});
import AnnouncementBanner from './announcement_banner.js';
@@ -12,13 +12,15 @@ jest.useFakeTimers();
describe('AnnouncementBanner', () => {
const baseProps = {
actions: {
dismissBanner: jest.fn(),
},
allowDismissal: true,
bannerColor: '#ddd',
bannerDismissed: false,
bannerEnabled: true,
bannerText: 'Banner Text',
bannerTextColor: '#fff',
navigator: {},
theme: Preferences.THEMES.default,
};
test('should match snapshot', () => {
@@ -26,9 +28,21 @@ describe('AnnouncementBanner', () => {
<AnnouncementBanner {...baseProps}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
wrapper.setProps({bannerEnabled: false});
expect(wrapper.getElement()).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
});
});
test('should call actions.dismissBanner on handleDismiss', () => {
const actions = {dismissBanner: jest.fn()};
const props = {...baseProps, actions};
const wrapper = shallow(
<AnnouncementBanner {...props}/>
);
wrapper.instance().handleDismiss();
expect(actions.dismissBanner).toHaveBeenCalledTimes(1);
expect(actions.dismissBanner).toHaveBeenCalledWith(props.bannerText);
});
});

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

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 from 'react';
import PropTypes from 'prop-types';
@@ -8,8 +8,6 @@ import {intlShape} from 'react-intl';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import {emptyFunction} from 'app/utils/general';
import CustomPropTypes from 'app/constants/custom_prop_types';
import mattermostManaged from 'app/mattermost_managed';
@@ -27,10 +25,6 @@ export default class AtMention extends React.PureComponent {
usersByUsername: PropTypes.object.isRequired,
};
static defaultProps = {
onLongPress: emptyFunction,
};
static contextTypes = {
intl: intlShape,
};
@@ -80,7 +74,7 @@ export default class AtMention extends React.PureComponent {
};
getUserDetailsFromMentionName(props) {
let mentionName = props.mentionName.toLowerCase();
let mentionName = props.mentionName;
while (mentionName.length > 0) {
if (props.usersByUsername.hasOwnProperty(mentionName)) {

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,3 @@
// 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';
@@ -10,8 +8,6 @@ import {
StyleSheet,
TouchableOpacity,
} from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
import Icon from 'react-native-vector-icons/Ionicons';
import {DocumentPicker} from 'react-native-document-picker';
import ImagePicker from 'react-native-image-picker';
@@ -19,7 +15,6 @@ import Permissions from 'react-native-permissions';
import {PermissionTypes} from 'app/constants';
import {changeOpacity} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
const ShareExtension = NativeModules.MattermostShare;
@@ -29,10 +24,8 @@ export default class AttachmentButton extends PureComponent {
children: PropTypes.node,
fileCount: PropTypes.number,
maxFileCount: PropTypes.number.isRequired,
maxFileSize: PropTypes.number.isRequired,
navigator: PropTypes.object.isRequired,
onShowFileMaxWarning: PropTypes.func,
onShowFileSizeWarning: PropTypes.func,
theme: PropTypes.object.isRequired,
uploadFiles: PropTypes.func.isRequired,
wrapper: PropTypes.bool,
@@ -46,17 +39,11 @@ export default class AttachmentButton extends PureComponent {
intl: intlShape.isRequired,
};
attachPhotoFromCamera = () => {
return this.attachFileFromCamera('photo');
};
attachFileFromCamera = async (mediaType) => {
attachFileFromCamera = async () => {
const {formatMessage} = this.context.intl;
const options = {
quality: 0.8,
videoQuality: 'high',
quality: 1.0,
noData: true,
mediaType,
storageOptions: {
cameraRoll: true,
waitUntilSaved: true,
@@ -94,7 +81,7 @@ export default class AttachmentButton extends PureComponent {
attachFileFromLibrary = () => {
const {formatMessage} = this.context.intl;
const options = {
quality: 0.8,
quality: 1.0,
noData: true,
permissionDenied: {
title: formatMessage({
@@ -126,14 +113,10 @@ export default class AttachmentButton extends PureComponent {
});
};
attachVideoFromCamera = () => {
return this.attachFileFromCamera('video');
};
attachVideoFromLibraryAndroid = () => {
const {formatMessage} = this.context.intl;
const options = {
videoQuality: 'high',
quality: 1.0,
mediaType: 'video',
noData: true,
permissionDenied: {
@@ -297,20 +280,8 @@ export default class AttachmentButton extends PureComponent {
return true;
};
uploadFiles = async (files) => {
const file = files[0];
if (!file.fileSize | !file.fileName) {
const path = (file.path || file.uri).replace('file://', '');
const fileInfo = await RNFetchBlob.fs.stat(path);
file.fileSize = fileInfo.size;
file.fileName = fileInfo.filename;
}
if (file.fileSize > this.props.maxFileSize) {
this.props.onShowFileSizeWarning(file.fileName);
} else {
this.props.uploadFiles(files);
}
uploadFiles = (images) => {
this.props.uploadFiles(images);
};
handleFileAttachmentOption = (action) => {
@@ -339,23 +310,16 @@ export default class AttachmentButton extends PureComponent {
this.props.blurTextBox();
const options = {
items: [{
action: () => this.handleFileAttachmentOption(this.attachPhotoFromCamera),
action: () => this.handleFileAttachmentOption(this.attachFileFromCamera),
text: {
id: t('mobile.file_upload.camera_photo'),
defaultMessage: 'Take Photo',
id: 'mobile.file_upload.camera',
defaultMessage: 'Take Photo or Video',
},
icon: 'camera',
}, {
action: () => this.handleFileAttachmentOption(this.attachVideoFromCamera),
text: {
id: t('mobile.file_upload.camera_video'),
defaultMessage: 'Take Video',
},
icon: 'video-camera',
}, {
action: () => this.handleFileAttachmentOption(this.attachFileFromLibrary),
text: {
id: t('mobile.file_upload.library'),
id: 'mobile.file_upload.library',
defaultMessage: 'Photo Library',
},
icon: 'photo',
@@ -366,7 +330,7 @@ export default class AttachmentButton extends PureComponent {
options.items.push({
action: () => this.handleFileAttachmentOption(this.attachVideoFromLibraryAndroid),
text: {
id: t('mobile.file_upload.video'),
id: 'mobile.file_upload.video',
defaultMessage: 'Video Library',
},
icon: 'file-video-o',
@@ -376,7 +340,7 @@ export default class AttachmentButton extends PureComponent {
options.items.push({
action: () => this.handleFileAttachmentOption(this.attachFileFromFiles),
text: {
id: t('mobile.file_upload.browse'),
id: 'mobile.file_upload.browse',
defaultMessage: 'Browse Files',
},
icon: 'file',

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';
@@ -13,7 +13,6 @@ import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divide
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
import SpecialMentionItem from 'app/components/autocomplete/special_mention_item';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
export default class AtMention extends PureComponent {
static propTypes = {
@@ -26,8 +25,8 @@ export default class AtMention extends PureComponent {
defaultChannel: PropTypes.object,
inChannel: PropTypes.array,
isSearch: PropTypes.bool,
listHeight: PropTypes.number,
matchTerm: PropTypes.string,
maxListHeight: PropTypes.number,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
outChannel: PropTypes.array,
@@ -82,7 +81,7 @@ export default class AtMention extends PureComponent {
const sections = [];
if (isSearch) {
sections.push({
id: t('mobile.suggestion.members'),
id: 'mobile.suggestion.members',
defaultMessage: 'Members',
data: teamMembers,
key: 'teamMembers',
@@ -90,7 +89,7 @@ export default class AtMention extends PureComponent {
} else {
if (inChannel.length) {
sections.push({
id: t('suggestion.mention.members'),
id: 'suggestion.mention.members',
defaultMessage: 'Channel Members',
data: inChannel,
key: 'inChannel',
@@ -99,7 +98,7 @@ export default class AtMention extends PureComponent {
if (this.checkSpecialMentions(matchTerm)) {
sections.push({
id: t('suggestion.mention.special'),
id: 'suggestion.mention.special',
defaultMessage: 'Special Mentions',
data: this.getSpecialMentions(),
key: 'special',
@@ -109,7 +108,7 @@ export default class AtMention extends PureComponent {
if (outChannel.length) {
sections.push({
id: t('suggestion.mention.nonmembers'),
id: 'suggestion.mention.nonmembers',
defaultMessage: 'Not in Channel',
data: outChannel,
key: 'outChannel',
@@ -132,18 +131,18 @@ export default class AtMention extends PureComponent {
getSpecialMentions = () => {
return [{
completeHandle: 'all',
id: t('suggestion.mention.all'),
id: 'suggestion.mention.all',
defaultMessage: 'Notifies everyone in the channel, use in {townsquare} to notify the whole team',
values: {
townsquare: this.props.defaultChannel.display_name,
},
}, {
completeHandle: 'channel',
id: t('suggestion.mention.channel'),
id: 'suggestion.mention.channel',
defaultMessage: 'Notifies everyone in the channel',
}, {
completeHandle: 'here',
id: t('suggestion.mention.here'),
id: 'suggestion.mention.here',
defaultMessage: 'Notifies everyone in the channel and online',
}];
};
@@ -204,7 +203,7 @@ export default class AtMention extends PureComponent {
};
render() {
const {maxListHeight, theme} = this.props;
const {isSearch, listHeight, theme} = this.props;
const {mentionComplete, sections} = this.state;
if (sections.length === 0 || mentionComplete) {
@@ -219,7 +218,7 @@ export default class AtMention extends PureComponent {
<SectionList
keyboardShouldPersistTaps='always'
keyExtractor={this.keyExtractor}
style={[style.listView, {maxHeight: maxListHeight}]}
style={[style.listView, isSearch ? [style.search, {height: listHeight}] : null]}
sections={sections}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
@@ -235,5 +234,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
listView: {
backgroundColor: theme.centerChannelBg,
},
search: {
minHeight: 125,
},
};
});

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.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
@@ -16,25 +16,21 @@ import AtMention from './at_mention';
import ChannelMention from './channel_mention';
import EmojiSuggestion from './emoji_suggestion';
import SlashSuggestion from './slash_suggestion';
import DateSuggestion from './date_suggestion';
export default class Autocomplete extends PureComponent {
static propTypes = {
cursorPosition: PropTypes.number.isRequired,
deviceHeight: PropTypes.number,
onChangeText: PropTypes.func.isRequired,
maxHeight: PropTypes.number,
rootId: PropTypes.string,
isSearch: PropTypes.bool,
theme: PropTypes.object.isRequired,
value: PropTypes.string,
enableDateSuggestion: PropTypes.bool.isRequired,
};
static defaultProps = {
isSearch: false,
cursorPosition: 0,
enableDateSuggestion: false,
};
state = {
@@ -42,7 +38,6 @@ export default class Autocomplete extends PureComponent {
channelMentionCount: 0,
emojiCount: 0,
commandCount: 0,
dateCount: 0,
keyboardOffset: 0,
};
@@ -62,10 +57,6 @@ export default class Autocomplete extends PureComponent {
this.setState({commandCount});
};
handleIsDateFilterChange = (dateCount) => {
this.setState({dateCount});
};
componentWillMount() {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide);
@@ -85,24 +76,19 @@ export default class Autocomplete extends PureComponent {
this.setState({keyboardOffset: 0});
};
maxListHeight() {
let maxHeight;
if (this.props.maxHeight) {
maxHeight = this.props.maxHeight;
} else {
// List is expanding downwards, likely from the search box
let offset = Platform.select({ios: 65, android: 75});
if (DeviceInfo.getModel().includes('iPhone X')) {
offset = 90;
}
maxHeight = this.props.deviceHeight - offset - this.state.keyboardOffset;
listHeight() {
let offset = Platform.select({ios: 65, android: 75});
if (DeviceInfo.getModel() === 'iPhone X') {
offset = 90;
}
return maxHeight;
return this.props.deviceHeight - offset - this.state.keyboardOffset;
}
render() {
if (!this.props.value) {
return null;
}
const style = getStyleFromTheme(this.props.theme);
const wrapperStyle = [];
@@ -115,46 +101,36 @@ export default class Autocomplete extends PureComponent {
}
// We always need to render something, but we only draw the borders when we have results to show
const {atMentionCount, channelMentionCount, emojiCount, commandCount, dateCount} = this.state;
if (atMentionCount + channelMentionCount + emojiCount + commandCount + dateCount > 0) {
const {atMentionCount, channelMentionCount, emojiCount, commandCount} = this.state;
if (atMentionCount + channelMentionCount + emojiCount + commandCount > 0) {
if (this.props.isSearch) {
wrapperStyle.push(style.bordersSearch);
} else {
containerStyle.push(style.borders);
}
}
const maxListHeight = this.maxListHeight();
const listHeight = this.listHeight();
return (
<View style={wrapperStyle}>
<View style={containerStyle}>
<AtMention
maxListHeight={maxListHeight}
listHeight={listHeight}
onResultCountChange={this.handleAtMentionCountChange}
{...this.props}
/>
<ChannelMention
maxListHeight={maxListHeight}
listHeight={listHeight}
onResultCountChange={this.handleChannelMentionCountChange}
{...this.props}
/>
<EmojiSuggestion
maxListHeight={maxListHeight}
onResultCountChange={this.handleEmojiCountChange}
{...this.props}
/>
<SlashSuggestion
maxListHeight={maxListHeight}
onResultCountChange={this.handleCommandCountChange}
{...this.props}
/>
{(this.props.isSearch && this.props.enableDateSuggestion) &&
<DateSuggestion
onResultCountChange={this.handleIsDateFilterChange}
{...this.props}
/>
}
</View>
</View>
);
@@ -180,6 +156,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
},
container: {
bottom: 0,
maxHeight: 200,
},
content: {
flex: 1,

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';
@@ -39,6 +39,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
justifyContent: 'center',
paddingLeft: 8,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
},
sectionText: {
fontSize: 12,

View File

@@ -1,45 +1,37 @@
// 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';
import {Platform, SectionList} from 'react-native';
import {SectionList} from 'react-native';
import {RequestStatus} from 'mattermost-redux/constants';
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
import {debounce} from 'mattermost-redux/actions/helpers';
import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
import ChannelMentionItem from 'app/components/autocomplete/channel_mention_item';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
export default class ChannelMention extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
searchChannels: PropTypes.func.isRequired,
autocompleteChannelsForSearch: PropTypes.func.isRequired,
}).isRequired,
currentTeamId: PropTypes.string.isRequired,
cursorPosition: PropTypes.number.isRequired,
isSearch: PropTypes.bool,
listHeight: PropTypes.number,
matchTerm: PropTypes.string,
maxListHeight: PropTypes.number,
myChannels: PropTypes.array,
myMembers: PropTypes.object,
otherChannels: PropTypes.array,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
privateChannels: PropTypes.array,
publicChannels: PropTypes.array,
directAndGroupMessages: PropTypes.array,
deletedPublicChannels: PropTypes.instanceOf(Set),
requestStatus: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
value: PropTypes.string,
serverVersion: PropTypes.string,
};
static defaultProps = {
@@ -55,16 +47,8 @@ export default class ChannelMention extends PureComponent {
};
}
runSearch = debounce((currentTeamId, matchTerm) => {
if (isMinimumServerVersion(this.props.serverVersion, 5, 4)) {
this.props.actions.autocompleteChannelsForSearch(currentTeamId, matchTerm);
return;
}
this.props.actions.searchChannels(currentTeamId, matchTerm);
}, 200);
componentWillReceiveProps(nextProps) {
const {isSearch, matchTerm, myChannels, otherChannels, privateChannels, publicChannels, directAndGroupMessages, requestStatus, myMembers, deletedPublicChannels} = nextProps;
const {isSearch, matchTerm, myChannels, otherChannels, privateChannels, publicChannels, requestStatus} = nextProps;
if ((matchTerm !== this.props.matchTerm && matchTerm === null) || this.state.mentionComplete) {
// if the term changes but is null or the mention has been completed we render this component as null
@@ -84,48 +68,37 @@ export default class ChannelMention extends PureComponent {
if (matchTerm !== this.props.matchTerm) {
// if the term changed and we haven't made the request do that first
const {currentTeamId} = this.props;
this.runSearch(currentTeamId, matchTerm);
this.props.actions.searchChannels(currentTeamId, matchTerm);
return;
}
if (requestStatus !== RequestStatus.STARTED &&
(myChannels !== this.props.myChannels || otherChannels !== this.props.otherChannels ||
privateChannels !== this.props.privateChannels || publicChannels !== this.props.publicChannels ||
directAndGroupMessages !== this.props.directAndGroupMessages ||
myMembers !== this.props.myMembers || deletedPublicChannels !== this.props.deletedPublicChannels)) {
privateChannels !== this.props.privateChannels || publicChannels !== this.props.publicChannels)) {
// if the request is complete and the term is not null we show the autocomplete
const sections = [];
if (isSearch) {
if (publicChannels.length) {
sections.push({
id: t('suggestion.search.public'),
id: 'suggestion.search.public',
defaultMessage: 'Public Channels',
data: publicChannels.filter((cId) => myMembers[cId]),
data: publicChannels,
key: 'publicChannels',
});
}
if (privateChannels.length) {
sections.push({
id: t('suggestion.search.private'),
id: 'suggestion.search.private',
defaultMessage: 'Private Channels',
data: privateChannels,
key: 'privateChannels',
});
}
if (directAndGroupMessages.length && isMinimumServerVersion(this.props.serverVersion, 5, 4)) {
sections.push({
id: t('suggestion.search.direct'),
defaultMessage: 'Direct Messages',
data: directAndGroupMessages,
key: 'directAndGroupMessages',
});
}
} else {
if (myChannels.length) {
sections.push({
id: t('suggestion.mention.channels'),
id: 'suggestion.mention.channels',
defaultMessage: 'My Channels',
data: myChannels,
key: 'myChannels',
@@ -134,7 +107,7 @@ export default class ChannelMention extends PureComponent {
if (otherChannels.length) {
sections.push({
id: t('suggestion.mention.morechannels'),
id: 'suggestion.mention.morechannels',
defaultMessage: 'Other Channels',
data: otherChannels,
key: 'otherChannels',
@@ -158,10 +131,6 @@ export default class ChannelMention extends PureComponent {
if (isSearch) {
const channelOrIn = mentionPart.includes('in:') ? 'in:' : 'channel:';
completedDraft = mentionPart.replace(CHANNEL_MENTION_SEARCH_REGEX, `${channelOrIn} ${mention} `);
} else if (Platform.OS === 'ios') {
// We are going to set a double ~ on iOS to prevent the auto correct from taking over and replacing it
// with the wrong value, this is a hack but I could not found another way to solve it
completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~~${mention} `);
} else {
completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~${mention} `);
}
@@ -171,14 +140,6 @@ export default class ChannelMention extends PureComponent {
}
onChangeText(completedDraft, true);
if (Platform.OS === 'ios') {
// This is the second part of the hack were we replace the double ~ with just one
// after the auto correct vanished
setTimeout(() => {
onChangeText(completedDraft.replace(`~~${mention} `, `~${mention} `));
});
}
this.setState({mentionComplete: true});
};
@@ -206,7 +167,7 @@ export default class ChannelMention extends PureComponent {
};
render() {
const {maxListHeight, theme} = this.props;
const {isSearch, listHeight, theme} = this.props;
const {mentionComplete, sections} = this.state;
if (sections.length === 0 || mentionComplete) {
@@ -221,7 +182,7 @@ export default class ChannelMention extends PureComponent {
<SectionList
keyboardShouldPersistTaps='always'
keyExtractor={this.keyExtractor}
style={[style.listView, {maxHeight: maxListHeight}]}
style={[style.listView, isSearch ? [style.search, {height: listHeight}] : null]}
sections={sections}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
@@ -237,5 +198,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
listView: {
backgroundColor: theme.centerChannelBg,
},
search: {
minHeight: 125,
},
};
});

View File

@@ -1,11 +1,10 @@
// 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 {searchChannels, autocompleteChannelsForSearch} from 'mattermost-redux/actions/channels';
import {getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
import {searchChannels} from 'mattermost-redux/actions/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {
@@ -13,8 +12,6 @@ import {
filterOtherChannels,
filterPublicChannels,
filterPrivateChannels,
filterDirectAndGroupMessages,
getDeletedPublicChannelsIds,
getMatchTermForChannelMention,
} from 'app/selectors/autocomplete';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
@@ -31,11 +28,9 @@ function mapStateToProps(state, ownProps) {
let otherChannels;
let publicChannels;
let privateChannels;
let directAndGroupMessages;
if (isSearch) {
publicChannels = filterPublicChannels(state, matchTerm);
privateChannels = filterPrivateChannels(state, matchTerm);
directAndGroupMessages = filterDirectAndGroupMessages(state, matchTerm);
} else {
myChannels = filterMyChannels(state, matchTerm);
otherChannels = filterOtherChannels(state, matchTerm);
@@ -43,17 +38,13 @@ function mapStateToProps(state, ownProps) {
return {
myChannels,
myMembers: getMyChannelMemberships(state),
otherChannels,
publicChannels,
deletedPublicChannels: getDeletedPublicChannelsIds(state),
privateChannels,
directAndGroupMessages,
currentTeamId: getCurrentTeamId(state),
matchTerm,
requestStatus: state.requests.channels.getChannels.status,
theme: getTheme(state),
serverVersion: state.entities.general.serverVersion,
};
}
@@ -61,7 +52,6 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
searchChannels,
autocompleteChannelsForSearch,
}, dispatch),
};
}

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,8 +8,6 @@ import {
TouchableOpacity,
} from 'react-native';
import {General} from 'mattermost-redux/constants';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class ChannelMentionItem extends PureComponent {
@@ -17,20 +15,13 @@ export default class ChannelMentionItem extends PureComponent {
channelId: PropTypes.string.isRequired,
displayName: PropTypes.string,
name: PropTypes.string,
type: PropTypes.string,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
};
completeMention = () => {
const {onPress, displayName, name, type} = this.props;
if (type === General.DM_CHANNEL) {
onPress('@' + displayName.replace(/ /g, ''));
} else if (type === General.DM_CHANNEL) {
onPress('@' + displayName.replace(/ /g, ''));
} else {
onPress(name);
}
const {onPress, name} = this.props;
onPress(name);
};
render() {
@@ -39,22 +30,10 @@ export default class ChannelMentionItem extends PureComponent {
displayName,
name,
theme,
type,
} = this.props;
const style = getStyleFromTheme(theme);
if (type === General.DM_CHANNEL || type === General.GM_CHANNEL) {
return (
<TouchableOpacity
key={channelId}
onPress={this.completeMention}
style={style.row}
>
<Text style={style.rowDisplayName}>{'@' + displayName}</Text>
</TouchableOpacity>
);
}
return (
<TouchableOpacity
key={channelId}

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';
@@ -7,18 +7,14 @@ import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getChannelNameForSearchAutocomplete} from 'app/selectors/channel';
import ChannelMentionItem from './channel_mention_item';
function mapStateToProps(state, ownProps) {
const channel = getChannel(state, ownProps.channelId);
const displayName = getChannelNameForSearchAutocomplete(state, ownProps.channelId);
return {
displayName,
displayName: channel.display_name,
name: channel.name,
type: channel.type,
theme: getTheme(state),
};
}

View File

@@ -1,190 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Dimensions, Platform, StyleSheet} from 'react-native';
import PropTypes from 'prop-types';
import {CalendarList, LocaleConfig} from 'react-native-calendars';
import {intlShape} from 'react-intl';
import {memoizeResult} from 'mattermost-redux/utils/helpers';
import {DATE_MENTION_SEARCH_REGEX, ALL_SEARCH_FLAGS_REGEX} from 'app/constants/autocomplete';
import {changeOpacity} from 'app/utils/theme';
export default class DateSuggestion extends PureComponent {
static propTypes = {
cursorPosition: PropTypes.number.isRequired,
locale: PropTypes.string.isRequired,
matchTerm: PropTypes.string,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
value: PropTypes.string,
enableDateSuggestion: PropTypes.bool.isRequired,
};
static defaultProps = {
value: '',
};
static contextTypes = {
intl: intlShape.isRequired,
};
constructor(props) {
super(props);
this.state = {
mentionComplete: false,
sections: [],
};
}
componentDidMount() {
this.setCalendarLocale();
}
componentWillReceiveProps(nextProps) {
const {matchTerm} = nextProps;
if ((matchTerm !== this.props.matchTerm && matchTerm === null) || this.state.mentionComplete) {
// if the term changes but is null or the mention has been completed we render this component as null
this.setState({
mentionComplete: false,
sections: [],
});
this.props.onResultCountChange(0);
}
if (this.props.locale !== nextProps.locale) {
this.setCalendarLocale(nextProps);
}
}
completeMention = (day) => {
const mention = day.dateString;
const {cursorPosition, onChangeText, value} = this.props;
const mentionPart = value.substring(0, cursorPosition);
const flags = mentionPart.match(ALL_SEARCH_FLAGS_REGEX);
const currentFlag = flags[flags.length - 1];
let completedDraft = mentionPart.replace(DATE_MENTION_SEARCH_REGEX, `${currentFlag} ${mention} `);
if (value.length > cursorPosition) {
completedDraft += value.substring(cursorPosition);
}
onChangeText(completedDraft, true);
this.props.onResultCountChange(1);
this.setState({mentionComplete: true});
};
setCalendarLocale = (props = this.props) => {
const {formatMessage} = this.context.intl;
LocaleConfig.locales[props.locale] = {
monthNames: formatMessage({
id: 'mobile.calendar.monthNames',
defaultMessage: 'January,February,March,April,May,June,July,August,September,October,November,December',
}).split(','),
monthNamesShort: formatMessage({
id: 'mobile.calendar.monthNamesShort',
defaultMessage: 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec',
}).split(','),
dayNames: formatMessage({
id: 'mobile.calendar.dayNames',
defaultMessage: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday',
}).split(','),
dayNamesShort: formatMessage({
id: 'mobile.calendar.dayNamesShort',
defaultMessage: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat',
}).split(','),
};
LocaleConfig.defaultLocale = props.locale;
};
render() {
const {mentionComplete} = this.state;
const {matchTerm, enableDateSuggestion, theme} = this.props;
if (matchTerm === null || mentionComplete || !enableDateSuggestion) {
// If we are not in an active state or the mention has been completed return null so nothing is rendered
// other components are not blocked.
return null;
}
const currentDate = (new Date()).toDateString();
const calendarStyle = calendarTheme(theme);
return (
<CalendarList
style={styles.calList}
current={currentDate}
pastScrollRange={24}
futureScrollRange={0}
scrollingEnabled={true}
pagingEnabled={true}
hideArrows={false}
horizontal={true}
showScrollIndicator={true}
onDayPress={this.completeMention}
showWeekNumbers={false}
theme={calendarStyle}
keyboardShouldPersistTaps='always'
/>
);
}
}
const getDateFontSize = () => {
let fontSize = 14;
if (Platform.OS === 'ios') {
const {height, width} = Dimensions.get('window');
if (height < 375 || width < 375) {
fontSize = 13;
}
}
return fontSize;
};
const calendarTheme = memoizeResult((theme) => ({
calendarBackground: theme.centerChannelBg,
monthTextColor: changeOpacity(theme.centerChannelColor, 0.8),
dayTextColor: theme.centerChannelColor,
textSectionTitleColor: changeOpacity(theme.centerChannelColor, 0.25),
'stylesheet.day.basic': {
base: {
width: 22,
height: 22,
alignItems: 'center',
},
text: {
marginTop: 0,
fontSize: getDateFontSize(),
fontWeight: '300',
color: theme.centerChannelColor,
backgroundColor: 'rgba(255, 255, 255, 0)',
lineHeight: 23,
},
today: {
backgroundColor: theme.buttonBg,
width: 24,
height: 24,
borderRadius: 12,
},
todayText: {
color: theme.buttonColor,
},
},
}));
const styles = StyleSheet.create({
calList: {
height: 1700,
paddingTop: 5,
},
});

View File

@@ -1,30 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {makeGetMatchTermForDateMention} from 'app/selectors/autocomplete';
import {getCurrentLocale} from 'app/selectors/i18n';
import DateSuggestion from './date_suggestion';
function makeMapStateToProps() {
const getMatchTermForDateMention = makeGetMatchTermForDateMention();
return (state, ownProps) => {
const {cursorPosition, value} = ownProps;
const newValue = value.substring(0, cursorPosition);
const matchTerm = getMatchTermForDateMention(newValue);
return {
matchTerm,
locale: getCurrentLocale(state),
theme: getTheme(state),
};
};
}
export default connect(makeMapStateToProps)(DateSuggestion);

View File

@@ -1,11 +1,10 @@
// 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 {
FlatList,
Platform,
Text,
TouchableOpacity,
View,
@@ -30,7 +29,6 @@ export default class EmojiSuggestion extends Component {
emojis: PropTypes.array.isRequired,
isSearch: PropTypes.bool,
fuse: PropTypes.object.isRequired,
maxListHeight: PropTypes.number,
theme: PropTypes.object.isRequired,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
@@ -134,28 +132,13 @@ export default class EmojiSuggestion extends Component {
actions.addReactionToLatestPost(emoji, rootId);
onChangeText('');
} else {
// We are going to set a double : on iOS to prevent the auto correct from taking over and replacing it
// with the wrong value, this is a hack but I could not found another way to solve it
let completedDraft;
if (Platform.OS === 'ios') {
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `::${emoji}: `);
} else {
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `:${emoji}: `);
}
let completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `:${emoji}: `);
if (value.length > cursorPosition) {
completedDraft += value.substring(cursorPosition);
}
onChangeText(completedDraft);
if (Platform.OS === 'ios') {
// This is the second part of the hack were we replace the double : with just one
// after the auto correct vanished
setTimeout(() => {
onChangeText(completedDraft.replace(`::${emoji}: `, `:${emoji}: `));
});
}
}
this.setState({
@@ -188,20 +171,18 @@ export default class EmojiSuggestion extends Component {
getItemLayout = ({index}) => ({length: 40, offset: 40 * index, index})
render() {
const {maxListHeight, theme} = this.props;
if (!this.state.active) {
// If we are not in an active state return null so nothing is rendered
// other components are not blocked.
return null;
}
const style = getStyleFromTheme(theme);
const style = getStyleFromTheme(this.props.theme);
return (
<FlatList
keyboardShouldPersistTaps='always'
style={[style.listView, {maxHeight: maxListHeight}]}
style={style.listView}
extraData={this.state}
data={this.state.dataSource}
keyExtractor={this.keyExtractor}

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';
@@ -7,6 +7,7 @@ import {bindActionCreators} from 'redux';
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
import {autocompleteCustomEmojis} from 'mattermost-redux/actions/emojis';
import {Client4} from 'mattermost-redux/client';
import {addReactionToLatestPost} from 'app/actions/views/emoji';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
@@ -45,7 +46,7 @@ function mapStateToProps(state) {
fuse,
emojis,
theme: getTheme(state),
serverVersion: state.entities.general.serverVersion,
serverVersion: state.entities.general.serverVersion || Client4.getServerVersion(),
};
}

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,16 +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 React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
FlatList,
Platform,
} from 'react-native';
import {RequestStatus} from 'mattermost-redux/constants';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import SlashSuggestionItem from './slash_suggestion_item';
@@ -27,7 +25,6 @@ export default class SlashSuggestion extends Component {
commands: PropTypes.array,
commandsRequest: PropTypes.object.isRequired,
isSearch: PropTypes.bool,
maxListHeight: PropTypes.number,
theme: PropTypes.object.isRequired,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
@@ -113,23 +110,10 @@ export default class SlashSuggestion extends Component {
completeSuggestion = (command) => {
const {onChangeText} = this.props;
// We are going to set a double / on iOS to prevent the auto correct from taking over and replacing it
// with the wrong value, this is a hack but I could not found another way to solve it
let completedDraft = `/${command} `;
if (Platform.OS === 'ios') {
completedDraft = `//${command} `;
}
const completedDraft = `/${command} `;
onChangeText(completedDraft);
if (Platform.OS === 'ios') {
// This is the second part of the hack were we replace the double / with just one
// after the auto correct vanished
setTimeout(() => {
onChangeText(completedDraft.replace(`//${command} `, `/${command} `));
});
}
this.setState({
active: false,
suggestionComplete: true,
@@ -140,6 +124,7 @@ export default class SlashSuggestion extends Component {
renderItem = ({item}) => (
<SlashSuggestionItem
displayName={item.display_name}
description={item.auto_complete_desc}
hint={item.auto_complete_hint}
onPress={this.completeSuggestion}
@@ -149,25 +134,22 @@ export default class SlashSuggestion extends Component {
)
render() {
const {maxListHeight, theme} = this.props;
if (!this.state.active) {
// If we are not in an active state return null so nothing is rendered
// other components are not blocked.
return null;
}
const style = getStyleFromTheme(theme);
const style = getStyleFromTheme(this.props.theme);
return (
<FlatList
keyboardShouldPersistTaps='always'
style={[style.listView, {maxHeight: maxListHeight}]}
style={style.listView}
extraData={this.state}
data={this.state.dataSource}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
ItemSeparatorComponent={AutocompleteDivider}
pageSize={10}
initialListSize={10}
/>

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';
@@ -12,6 +12,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class SlashSuggestionItem extends PureComponent {
static propTypes = {
displayName: PropTypes.string,
description: PropTypes.string,
hint: PropTypes.string,
onPress: PropTypes.func.isRequired,
@@ -26,6 +27,7 @@ export default class SlashSuggestionItem extends PureComponent {
render() {
const {
displayName,
description,
hint,
theme,
@@ -39,7 +41,7 @@ export default class SlashSuggestionItem extends PureComponent {
onPress={this.completeSuggestion}
style={style.row}
>
<Text style={style.suggestionName}>{`/${trigger} ${hint}`}</Text>
<Text style={style.suggestionName}>{`/${displayName || trigger} ${hint}`}</Text>
<Text style={style.suggestionDescription}>{description}</Text>
</TouchableOpacity>
);
@@ -53,6 +55,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
justifyContent: 'center',
paddingHorizontal: 8,
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,

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

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