Compare commits

..

23 Commits

Author SHA1 Message Date
Weblate (bot)
ee34b4925b Translations update from Weblate (#4925)
* Translated using Weblate (Russian)

Currently translated at 100.0% (655 of 655 strings)

Co-authored-by: Alexey Napalkov <flynbit@gmail.com>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/ru/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

* Translated using Weblate (Turkish)

Currently translated at 100.0% (655 of 655 strings)

Co-authored-by: Kaya Zeren <kayazeren@gmail.com>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/tr/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

* Translated using Weblate (Spanish)

Currently translated at 100.0% (655 of 655 strings)

Co-authored-by: Eva Gamundi <evagamundi@gmail.com>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/es/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

Co-authored-by: Alexey Napalkov <flynbit@gmail.com>
Co-authored-by: Kaya Zeren <kayazeren@gmail.com>
Co-authored-by: Eva Gamundi <evagamundi@gmail.com>
2020-10-27 10:44:55 +01:00
Weblate (bot)
bf9fc6e2bf Translations update from Weblate (#4899)
* Translated using Weblate (Korean)

Currently translated at 79.6% (522 of 655 strings)

Co-authored-by: retheviper <youngbina@hotmail.com>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/ko/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

* Translated using Weblate (Korean)

Currently translated at 79.6% (522 of 655 strings)

Co-authored-by: Ji-Hyeon Gim <potatogim@potatogim.net>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/ko/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

Co-authored-by: retheviper <youngbina@hotmail.com>
Co-authored-by: Ji-Hyeon Gim <potatogim@potatogim.net>
2020-10-19 22:09:23 -03:00
Weblate (bot)
d5a408b967 Translations update from Weblate (#4886)
* Translated using Weblate (Korean)

Currently translated at 70.8% (464 of 655 strings)

Co-authored-by: Heefe <vampelf@gmail.com>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/ko/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

* Translated using Weblate (Spanish)

Currently translated at 100.0% (655 of 655 strings)

Co-authored-by: Elias  Nahum <elias@mattermost.com>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/es/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

* Translated using Weblate (Ukrainian)

Currently translated at 89.4% (586 of 655 strings)

Co-authored-by: Tatyana Rabievska <radyuk.tatyana@gmail.com>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/uk/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (655 of 655 strings)

Co-authored-by: aeomin <lin@aeomin.net>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/zh_Hans/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

* Translated using Weblate (German)

Currently translated at 96.1% (630 of 655 strings)

Co-authored-by: Elisabeth Kulzer <elikul@elikul.de>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/de/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

Co-authored-by: Heefe <vampelf@gmail.com>
Co-authored-by: Elias  Nahum <elias@mattermost.com>
Co-authored-by: Tatyana Rabievska <radyuk.tatyana@gmail.com>
Co-authored-by: aeomin <lin@aeomin.net>
Co-authored-by: Elisabeth Kulzer <elikul@elikul.de>
2020-10-13 07:16:48 -03:00
Weblate (bot)
95599b2c8a Translations update from Weblate (#4882)
* Translated using Weblate (Japanese)

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (649 of 650 strings)

Co-authored-by: kaakaa <stooner.hoe@gmail.com>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/ja/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (650 of 650 strings)

Co-authored-by: Chikei <chikei@gmail.com>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/zh_Hant/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

* Translated using Weblate (Dutch)

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (651 of 651 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (650 of 650 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (650 of 650 strings)

Co-authored-by: Tom De Moor <tom@controlaltdieliet.be>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/nl/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

* Translated using Weblate (Russian)

Currently translated at 100.0% (650 of 650 strings)

Co-authored-by: Alexey Napalkov <flynbit@gmail.com>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/ru/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

* Translated using Weblate (Romanian)

Currently translated at 100.0% (650 of 650 strings)

Co-authored-by: Viorel-Cosmin Miron <cosmin@uhlhost.net>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/ro/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (655 of 655 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (650 of 650 strings)

Co-authored-by: aeomin <lin@aeomin.net>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/zh_Hans/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

* Translated using Weblate (Turkish)

Currently translated at 100.0% (651 of 651 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (650 of 650 strings)

Co-authored-by: Kaya Zeren <kayazeren@gmail.com>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/tr/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

* Translated using Weblate (Dutch)

Currently translated at 100.0% (651 of 651 strings)

Co-authored-by: ctlaltdieliet_TESTACCOUNTFORWEBINARS <github3@controlaltdieliet.be>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/nl/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (655 of 655 strings)

Co-authored-by: rodrigocorsi <rodrigocorsi@gmail.com>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/pt_BR/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

* Translated using Weblate (Korean)

Currently translated at 69.7% (457 of 655 strings)

Co-authored-by: Heefe <vampelf@gmail.com>
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/ko/
Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36

* Translated using Weblate (Korean)

Currently translated at 70.3% (461 of 655 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_release-1.36
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_release-1-36/ko/

Co-authored-by: kaakaa <stooner.hoe@gmail.com>
Co-authored-by: Chikei <chikei@gmail.com>
Co-authored-by: Tom De Moor <tom@controlaltdieliet.be>
Co-authored-by: Alexey Napalkov <flynbit@gmail.com>
Co-authored-by: Viorel-Cosmin Miron <cosmin@uhlhost.net>
Co-authored-by: aeomin <lin@aeomin.net>
Co-authored-by: Kaya Zeren <kayazeren@gmail.com>
Co-authored-by: ctlaltdieliet_TESTACCOUNTFORWEBINARS <github3@controlaltdieliet.be>
Co-authored-by: rodrigocorsi <rodrigocorsi@gmail.com>
Co-authored-by: Heefe <vampelf@gmail.com>
2020-10-07 10:31:54 +02:00
Mattermost Build
349726641c Bump app build number to 330 (#4876) (#4877)
(cherry picked from commit 4782afc28e)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-10-05 13:51:15 -07:00
Mattermost Build
4602fb916f Emit PASTE_FILES event only from last subscribed input (#4872) (#4875)
(cherry picked from commit ae43ba1187)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-10-05 13:18:04 -07:00
Mattermost Build
c9ce2338f8 [MM-29275]: Remove first char of slash cmd autocomplete suggestion text, only if it is a top-level command (#4868) (#4873)
* Remove first char of suggestion text, only if it is a slash

* Check if suggestion is for a top-level command

(cherry picked from commit a6779f4293)

Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
2020-10-05 12:19:42 -07:00
Mattermost Build
a3dae17ded Bump app build number to 329 (#4864) (#4865)
(cherry picked from commit 2c7be6f0aa)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-10-02 11:04:58 -07:00
Mattermost Build
c815325034 [MM-29216] Don't allow sharing of files if file uploads are disabled (#4857) (#4862)
* Don't allow sharing of files if file uploads are disabled

* Remove snapshot

* Fix iOS message

(cherry picked from commit b8ce726b0d)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-10-02 10:12:41 -07:00
Mattermost Build
33cfb52bab [MM-29225] Linking fix (#4860) (#4861)
* Catch openURL over checking canOpenURL

* Update en.json

(cherry picked from commit a5cb92876c)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-10-02 10:12:29 -07:00
Mattermost Build
eeb26eac35 Update NOTICE.txt (#4836) (#4859)
(cherry picked from commit 07d47d90ce)

Co-authored-by: Amy Blais <amy_blais@hotmail.com>
2020-10-01 14:53:37 -07:00
Mattermost Build
e711b36789 [MM-29132] Update upload item styling (#4853) (#4856)
* Update upload item styling

* Use font size 9

(cherry picked from commit 85382e695a)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-10-01 11:36:56 -07:00
Mattermost Build
e9791b7bee Bump app build number to 328 (#4848) (#4849)
(cherry picked from commit 4265db3ecd)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-09-25 15:01:33 -07:00
Mattermost Build
d8ff0e52bb MM-22968 Restyle mobile autocomplete (#4531) (#4846)
* WIP: slash suggestion autocomplete

* WIP: patched styles a bit

* WIP: Adding styles

* Adding active state to autocomplete items

* Fixing style for channel mention item

* Fixing bugs + styling issues for Android

* Updating snapshot

* Fixing autocomplete to render on top of post draft

- Misc style fixes

* Renaming props, patching slash suggestion icon

* Fixing tests and lint errors

* Resolving post-merge issue with slash commands

* Fixing android positioning for autocomplete

* Fixing autocomplete not scrolling in edit_channel_info

* WIP: Fixing things according to UX Review

* UX Fixes to autocomplete

* Updating snapshots

* Updating snapshots, replacing slash-command icons

* Fixing android scrolling and positioning issues

* Fixing issues with date_suggestion not rendering

* Making use of the "ShowFullName" config in at_mention_item

* Removing top border on first autocomplete section

* Allowing autocomplete to be smaller than its maxWidth

* Fixing slash_suggestion padding

* removing "componentWillReceiveProps" from date_suggestion

* Changing edit_channel_info autocomplete offset

* Replacing toUpperCase() with textTransform: uppercase

* Fixing odd border issues + prop validation warning

* Restore section header background & add paddingBottom

* Patching up padding on channel mentions

- Reverting previous incorrect padding adjustments

* Removing inline 'completeSuggestion' function

* Removing brackets from style prop

(cherry picked from commit 59045e3bb0)

Co-authored-by: Andre Vasconcelos <andre.onogoro@gmail.com>
2020-09-25 11:39:38 -07:00
Mattermost Build
5778019074 [MM-28706] revert ordering change for in: autosuggest search (#4818) (#4844)
Summary:
Revert ordering change for in: auto-suggest search after user feedback. The ordering now is Public Channels, Private Channels, and finally DMs and GMs.
This PR only fixes ordering. Further search improvements are planned in https://mattermost.atlassian.net/browse/MM-26805

Ticket Link:
https://mattermost.atlassian.net/browse/MM-28706
The Jira ticket above reverts the changes in https://mattermost.atlassian.net/browse/MM-27540 (Parent: https://mattermost.atlassian.net/browse/MM-15477) which was implemented in #4704

(cherry picked from commit 99f018faac)

Co-authored-by: Ashish Bhate <bhate.ashish@gmail.com>
2020-09-25 13:58:04 +05:30
Mattermost Build
d67b7a2deb [MM-23909] better error message if url is empty (#4821) (#4842)
(cherry picked from commit b6fbcf2e7c)

Co-authored-by: Ashish Bhate <bhate.ashish@gmail.com>
2020-09-25 13:52:47 +05:30
Mattermost Build
dbefec6f17 [MM-28100] fix reachability check and ensure channel membership before getting posts (#4810) (#4841)
(cherry picked from commit 7647367bcf)

Co-authored-by: Ashish Bhate <bhate.ashish@gmail.com>
2020-09-25 13:46:17 +05:30
Mattermost Build
12fe0465c2 [MM-27615] Update RequiredServer (#4830) (#4840)
* Update RequiredServer

* Take 2

* Update patch version

* Take 3

* Update version

* Update version

(cherry picked from commit 72313291a1)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-09-24 11:13:53 -07:00
Mattermost Build
aa22d11f79 Exclude NotificationPreference for DMs (#4831) (#4839)
(cherry picked from commit 1d94fb6e16)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-09-24 11:07:52 -07:00
Mattermost Build
8cfa485f51 [MM-28973] Prevent destructuring of undefined (#4832) (#4838)
* Prevent destructuring of undefined

* Do the same for thread drafts

(cherry picked from commit 8a5c58b39a)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-09-24 11:00:58 -07:00
Miguel Alatzar
ae93498d50 Bump app build number to 327 (#4829) 2020-09-22 10:56:59 -07:00
Miguel Alatzar
60385eeae8 Bump app version number to 1.36.0 (#4828) 2020-09-22 08:13:52 -07:00
Mattermost Build
22af5690ac Fix getPostThread import (#4819) (#4822)
(cherry picked from commit de1f38b839)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-09-21 11:05:30 -07:00
2033 changed files with 83761 additions and 177549 deletions

View File

@@ -1,7 +1,6 @@
version: 2.1
orbs:
owasp: entur/owasp@0.0.10
node: circleci/node@4.5.1
executors:
android:
@@ -14,7 +13,7 @@ executors:
NODE_ENV: production
BABEL_ENV: production
docker:
- image: circleci/android:api-30-node
- image: circleci/android:api-27-node
working_directory: ~/mattermost-mobile
resource_class: <<parameters.resource_class>>
@@ -24,7 +23,7 @@ executors:
NODE_ENV: production
BABEL_ENV: production
macos:
xcode: "13.0.0"
xcode: "11.0.0"
working_directory: ~/mattermost-mobile
shell: /bin/bash --login -o pipefail
@@ -45,6 +44,7 @@ commands:
for:
type: string
steps:
- ruby-setup
- restore_cache:
name: Restore Fastlane cache
key: v1-gems-<< parameters.for >>-{{ checksum "fastlane/Gemfile.lock" }}-{{ arch }}
@@ -82,7 +82,7 @@ commands:
key: v1-assets-{{ checksum "assets/base/config.json" }}-{{ arch }}
- run:
name: Generate assets
command: node ./scripts/generate-assets.js
command: make dist/assets
- save_cache:
name: Save assets cache
key: v1-assets-{{ checksum "assets/base/config.json" }}-{{ arch }}
@@ -92,23 +92,20 @@ commands:
npm-dependencies:
description: "Get JavaScript dependencies"
steps:
- node/install:
node-version: '16.2.0'
install-npm: false
- restore_cache:
name: Restore npm cache
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
- run:
name: Getting JavaScript dependencies
command: NODE_ENV=development npm ci --ignore-scripts
command: NODE_ENV=development npm install --ignore-scripts
- save_cache:
name: Save npm cache
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
paths:
- node_modules
- run:
name: "Patch dependencies"
command: npx patch-package
name: "Run post install scripts"
command: make post-install
pods-dependencies:
description: "Get cocoapods dependencies"
@@ -116,12 +113,10 @@ commands:
- restore_cache:
name: Restore cocoapods specs and pods
key: v1-cocoapods-{{ checksum "ios/Podfile.lock" }}-{{ arch }}
- run:
name: iOS gems
command: npm run ios-gems
- run:
name: Getting cocoapods dependencies
command: npm run pod-install
working_directory: ios
command: pod install
- save_cache:
name: Save cocoapods specs and pods cache
key: v1-cocoapods-{{ checksum "ios/Podfile.lock" }}-{{ arch }}
@@ -148,14 +143,11 @@ commands:
echo MATTERMOST_RELEASE_STORE_FILE=${STORE_FILE} | tee -a android/gradle.properties > /dev/null
echo ${STORE_ALIAS} | tee -a android/gradle.properties > /dev/null
echo ${STORE_PASSWORD} | tee -a android/gradle.properties > /dev/null
- run:
name: Jetify android libraries
command: ./node_modules/.bin/jetify
- run:
working_directory: fastlane
name: Run fastlane to build android
no_output_timeout: 30m
command: export TERM=xterm && bundle exec fastlane android build
command: bundle exec fastlane android build
build-ios:
description: "Build the iOS app"
@@ -173,7 +165,7 @@ commands:
no_output_timeout: 30m
command: |
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
export TERM=xterm && bundle exec fastlane ios build
bundle exec fastlane ios build
deploy-to-store:
description: "Deploy build to store"
@@ -206,13 +198,14 @@ commands:
filename:
type: string
steps:
- run:
name: Copying artifacts
command: |
mkdir /tmp/artifacts;
cp ~/mattermost-mobile/<<parameters.filename>> /tmp/artifacts;
- store_artifacts:
path: /tmp/artifacts
path: ~/mattermost-mobile/<<parameters.filename>>
ruby-setup:
steps:
- run:
name: Set Ruby Version
command: echo "ruby-2.6.3" > ~/.ruby-version
jobs:
test:
@@ -232,7 +225,19 @@ jobs:
command: npm test
- run:
name: Check i18n
command: ./scripts/precommit/i18n.sh
command: |
mkdir -p tmp
cp assets/base/i18n/en.json tmp/en.json
mkdir -p tmp/fake-webapp-dir/i18n/
echo '{}' > tmp/fake-webapp-dir/i18n/en.json
npm run mmjstool -- i18n extract-mobile --webapp-dir tmp/fake-webapp-dir --mobile-dir .
diff tmp/en.json assets/base/i18n/en.json
# Address weblate behavior which does not remove whole translation item when translation string is set to empty
npm run mmjstool -- i18n clean-empty --webapp-dir tmp/fake-webapp-dir --mobile-dir . --check
rm -rf tmp
check-deps:
parameters:
@@ -259,7 +264,7 @@ jobs:
steps:
# Taken from https://github.com/entur/owasp-orb/blob/master/src/%40orb.yml#L349-L361
- owasp/generate_cache_keys:
cache_key: commmandline-default-cache-key-v7
cache_key: commmandline-default-cache-key-v6
- owasp/restore_owasp_cache
- run:
name: Update OWASP Dependency-Check Database
@@ -292,7 +297,7 @@ jobs:
- build-android
- persist
- save:
filename: "*.apk"
filename: "Mattermost_Beta.apk"
build-android-release:
executor: android
@@ -300,7 +305,7 @@ jobs:
- build-android
- persist
- save:
filename: "*.apk"
filename: "Mattermost.apk"
build-android-pr:
executor: android
@@ -309,7 +314,7 @@ jobs:
steps:
- build-android
- save:
filename: "*.apk"
filename: "Mattermost_Beta.apk"
build-android-unsigned:
executor: android
@@ -321,9 +326,6 @@ jobs:
- fastlane-dependencies:
for: android
- gradle-dependencies
- run:
name: Jetify Android libraries
command: ./node_modules/.bin/jetify
- run:
working_directory: fastlane
name: Run fastlane to build unsigned android
@@ -331,7 +333,7 @@ jobs:
command: bundle exec fastlane android unsigned
- persist
- save:
filename: "*.apk"
filename: "Mattermost-unsigned.apk"
build-ios-beta:
executor: ios
@@ -339,7 +341,7 @@ jobs:
- build-ios
- persist
- save:
filename: "*.ipa"
filename: "Mattermost_Beta.ipa"
build-ios-release:
executor: ios
@@ -347,7 +349,7 @@ jobs:
- build-ios
- persist
- save:
filename: "*.ipa"
filename: "Mattermost.ipa"
build-ios-pr:
executor: ios
@@ -356,13 +358,14 @@ jobs:
steps:
- build-ios
- save:
filename: "*.ipa"
filename: "Mattermost_Beta.ipa"
build-ios-unsigned:
executor: ios
steps:
- checkout:
path: ~/mattermost-mobile
- ruby-setup
- npm-dependencies
- pods-dependencies
- assets
@@ -378,15 +381,16 @@ jobs:
- persist_to_workspace:
root: ~/
paths:
- mattermost-mobile/*.ipa
- mattermost-mobile/Mattermost-unsigned.ipa
- save:
filename: "*.ipa"
filename: "Mattermost-unsigned.ipa"
build-ios-simulator:
executor: ios
steps:
- checkout:
path: ~/mattermost-mobile
- ruby-setup
- npm-dependencies
- pods-dependencies
- assets
@@ -411,42 +415,47 @@ jobs:
name: android
resource_class: medium
steps:
- ruby-setup
- deploy-to-store:
task: "Deploy to Google Play"
target: android
file: "*.apk"
file: Mattermost.apk
deploy-android-beta:
executor:
name: android
resource_class: medium
steps:
- ruby-setup
- deploy-to-store:
task: "Deploy to Google Play"
target: android
file: "*.apk"
file: Mattermost_Beta.apk
deploy-ios-release:
executor: ios
steps:
- ruby-setup
- deploy-to-store:
task: "Deploy to TestFlight"
target: ios
file: "*.ipa"
file: Mattermost.ipa
deploy-ios-beta:
executor: ios
steps:
- ruby-setup
- deploy-to-store:
task: "Deploy to TestFlight"
target: ios
file: "*.ipa"
file: Mattermost_Beta.ipa
github-release:
executor:
name: android
resource_class: medium
steps:
- ruby-setup
- attach_workspace:
at: ~/
- run:
@@ -459,10 +468,10 @@ workflows:
build:
jobs:
- test
# - check-deps:
# context: sast-webhook
# requires:
# - test
- check-deps:
context: sast-webhook
requires:
- test
- build-android-release:
context: mattermost-mobile-android-release
@@ -554,14 +563,14 @@ workflows:
- test
filters:
branches:
only: /^(build|android)-pr-.*/
only: /^build-pr-.*/
- build-ios-pr:
context: mattermost-mobile-ios-pr
requires:
- test
filters:
branches:
only: /^(build|ios)-pr-.*/
only: /^build-pr-.*/
- build-android-unsigned:
context: mattermost-mobile-unsigned
@@ -591,7 +600,6 @@ workflows:
- /^build-\d+$/
- /^build-ios-\d+$/
- /^build-ios-beta-\d+$/
- /^build-ios-sim-\d+$/
- github-release:
context: mattermost-mobile-unsigned

2
.env
View File

@@ -1,2 +0,0 @@
STORYBOOK_PORT=
STORYBOOK_HOST=

View File

@@ -6,9 +6,8 @@
],
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"mattermost",
"import"
"@typescript-eslint"
],
"settings": {
"react": {
@@ -23,7 +22,6 @@
"__DEV__": true
},
"rules": {
"eol-last": ["error", "always"],
"global-require": 0,
"no-undefined": 0,
"react/display-name": [2, { "ignoreTranspilerName": false }],
@@ -44,43 +42,10 @@
}
],
"@typescript-eslint/no-explicit-any": "warn",
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": 0,
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/member-delimiter-style": 2,
"import/order": [
2,
{
"groups": ["builtin", "external", "parent", "sibling", "index", "type"],
"newlines-between": "always",
"pathGroups": [
{
"pattern": "@(@react-native-community|@react-native-cookies|@react-navigation|@rudderstack|@sentry|@testing-library|@storybook)/**",
"group": "external",
"position": "before"
},
{
"pattern": "@{**,*/**}",
"group": "external",
"position": "after"
},
{
"pattern": "app/**",
"group": "parent",
"position": "before"
}
],
"alphabetize": {
"order": "asc",
"caseInsensitive": true
},
"pathGroupsExcludedImportTypes": ["type"]
}
],
"no-shadow": "off",
"@typescript-eslint/no-shadow": "error"
"@typescript-eslint/explicit-module-boundary-types": "off"
},
"overrides": [
{

View File

@@ -8,6 +8,10 @@
; Ignore polyfills
node_modules/react-native/Libraries/polyfills/.*
; These should not be required directly
; require from fbjs/lib instead: require('fbjs/lib/warning')
node_modules/warning/.*
; Flow doesn't support platforms
.*/Libraries/Utilities/LoadingView.js
@@ -23,7 +27,8 @@ node_modules/react-native/flow/
[options]
emoji=true
exact_by_default=true
esproposal.optional_chaining=enable
esproposal.nullish_coalescing=enable
module.file_ext=.js
module.file_ext=.json
@@ -39,6 +44,10 @@ suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
[lints]
sketchy-null-number=warn
sketchy-null-mixed=warn
@@ -50,6 +59,7 @@ unsafe-getters-setters=warn
inexact-spread=warn
unnecessary-invariant=warn
signature-verification-failure=warn
deprecated-utility=error
[strict]
deprecated-type
@@ -61,4 +71,4 @@ untyped-import
untyped-type-import
[version]
^0.149.0
^0.122.0

7
.gitattributes vendored
View File

@@ -1,3 +1,4 @@
# Windows files should use crlf line endings
# https://help.github.com/articles/dealing-with-line-endings/
*.bat text eol=crlf
*.pbxproj -text
# specific for windows script files
*.bat text eol=crlf

View File

@@ -1,32 +0,0 @@
Per Mattermost guidelines, GitHub issues are for bug reports: <http://www.mattermost.org/filing-issues/>.
For troubleshooting see: http://forum.mattermost.org/.
For feature proposals see: http://www.mattermost.org/feature-requests/
If you've found a bug--something appears unintentional--please follow these steps:
1. Confirm youre filing a new issue. [Search existing tickets in Jira](https://mattermost.atlassian.net/jira/software/c/projects/MM/issues/) to ensure that the ticket does not already exist.
2. Confirm your issue does not involve security. Otherwise, please see our [Responsible Disclosure Policy](https://about.mattermost.com/report-security-issue/).
3. [File a new issue](https://github.com/mattermost/mattermost-mobile/issues/new) using the format below. Mattermost will confirm steps to reproduce and file in Jira, or ask for more details if there is trouble reproducing it. If there's already an existing bug in Jira, it will be linked back to the GitHub issue so you can track when it gets fixed.
#### Summary
Bug report in one concise sentence
### Environment Information
- Device Name:
- OS Version:
- Mattermost App Version:
- Mattermost Server Version:
#### Steps to reproduce
How can we reproduce the issue (what version are you using?)
#### Expected behavior
Describe your issue in detail
#### Observed behavior (that appears unintentional)
What did you see happen? Please include relevant error messages, screenshots and/or video recordings.
#### Possible fixes
If you can, link to the line of code that might be responsible for the problem

View File

@@ -1,61 +0,0 @@
<!-- Thank you for contributing a pull request! Here are a few tips to help you:
1. If this is your first contribution, make sure you've read the Contribution Checklist https://developers.mattermost.com/contribute/getting-started/contribution-checklist/
2. Read our blog post about "Submitting Great PRs" https://developers.mattermost.com/blog/2019-01-24-submitting-great-prs
3. Take a look at other repository specific documentation at https://developers.mattermost.com/contribute
-->
#### Summary
<!--
A brief description of what this pull request does.
-->
#### Ticket Link
<!--
If this pull request addresses a Help Wanted ticket or fixes a reported issue, please link the relevant GitHub issue, e.g.
Fixes https://github.com/mattermost/mattermost-mobile/issues/XXXXX
Otherwise, link the JIRA ticket.
-->
#### Checklist
<!--
Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fields.
-->
- [ ] Added or updated unit tests (required for all new features)
- [ ] Has UI changes
- [ ] Includes text changes and localization file updates
#### Device Information
This PR was tested on: <!-- Device name(s), OS version(s) -->
#### Screenshots
<!--
If the PR includes UI changes, include screenshots/GIFs/Videos (for both iOS and Android if possible).
-->
#### Release Note
<!--
Add a release note for each of the following conditions:
* New features and improvements, including behavioural changes, UI changes
* Bug fixes and fixes of previous known issues
* Deprecation warnings, breaking changes, or compatibility notes
If no release notes are required write NONE. Use past-tense. Newlines are stripped.
Example:
```release-note
Added a new config setting ServiceSettings.FooBar. Added a new column Foo to the Users table.
```
```release-note
NONE
```
-->
```release-note
```

View File

@@ -1,43 +0,0 @@
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '0 0 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

8
.gitignore vendored
View File

@@ -5,8 +5,6 @@ build-ios
server.PID
mattermost.keystore
tmp/
.env
env.d.ts
# OSX
#
@@ -91,12 +89,6 @@ ios/sentry.properties
coverage
.tmp
# E2E testing
mattermost-license.txt
*.mattermost-license
detox/artifacts
detox/detox_pixel_4_xl_api_30
# Bundle artifact
*.jsbundle

1
.husky/.gitignore vendored
View File

@@ -1 +0,0 @@
_

View File

@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
sh ./scripts/pre-commit.sh

View File

@@ -1,6 +0,0 @@
module.exports = {
"stories": [
"../app/components/**/*.stories.mdx",
"../app/components/**/*.stories.@(js|jsx|ts|tsx)"
],
}

View File

@@ -1,5 +0,0 @@
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
source "https://rubygems.org"
gem "cocoapods", "1.10.2"
gem "cocoapods", "1.7.5"

76
Gemfile.lock Normal file
View File

@@ -0,0 +1,76 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.2)
activesupport (4.2.11.1)
i18n (~> 0.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
atomos (0.1.3)
claide (1.0.3)
cocoapods (1.7.5)
activesupport (>= 4.0.2, < 5)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.7.5)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.2.2, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-stats (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.3.1, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.6.6)
nap (~> 1.0)
ruby-macho (~> 1.4)
xcodeproj (>= 1.10.0, < 2.0)
cocoapods-core (1.7.5)
activesupport (>= 4.0.2, < 6)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
cocoapods-deintegrate (1.0.4)
cocoapods-downloader (1.3.0)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.0)
cocoapods-stats (1.1.0)
cocoapods-trunk (1.4.1)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.1.0)
colored2 (3.1.2)
concurrent-ruby (1.1.5)
escape (0.0.4)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
minitest (5.14.0)
molinillo (0.6.6)
nanaimo (0.2.6)
nap (1.1.0)
netrc (0.11.0)
ruby-macho (1.4.0)
thread_safe (0.3.6)
tzinfo (1.2.6)
thread_safe (~> 0.1)
xcodeproj (1.15.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.2.6)
PLATFORMS
ruby
DEPENDENCIES
cocoapods (= 1.7.5)
BUNDLED WITH
2.0.2

232
Makefile Normal file
View File

@@ -0,0 +1,232 @@
.PHONY: pre-run pre-build clean
.PHONY: check-style
.PHONY: start stop
.PHONY: run run-ios run-android
.PHONY: build build-ios build-android unsigned-ios unsigned-android ios-sim-x86_64
.PHONY: build-pr can-build-pr prepare-pr
.PHONY: test help
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)
MM_UTILITIES_DIR = ../mattermost-utilities
node_modules: 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 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
.podinstall:
ifeq ($(OS), Darwin)
@echo "Required version of Cocoapods is not installed"
@echo Installing gems;
@bundle install
@echo Getting Cocoapods dependencies;
@cd ios && bundle exec pod install;
endif
@touch $@
dist/assets: $(BASE_ASSETS) $(OVERRIDE_ASSETS)
@mkdir -p dist
@if [ -e dist/assets ] ; then \
rm -rf dist/assets; \
fi
@echo "Generating app assets"
@node scripts/make-dist-assets.js
pre-run: | node_modules .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
@echo Checking for style guide compliance
@npm run check
clean: ## Cleans dependencies, previous builds and temp files
@echo Cleaning started
@rm -f .podinstall
@rm -rf ios/Pods
@rm -rf node_modules
@rm -rf dist
@rm -rf ios/build
@rm -rf android/app/build
@echo Cleanup finished
post-install:
@./node_modules/.bin/patch-package
@./node_modules/.bin/jetify
@rm -f node_modules/intl/.babelrc
@# Hack to get react-intl and its dependencies to work with react-native
@# Based off of https://github.com/este/este/blob/master/gulp/native-fix.js
@sed -i'' -e 's|"./locale-data/index.js": false|"./locale-data/index.js": "./locale-data/index.js"|g' node_modules/react-intl/package.json
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-messageformat/package.json
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-relativeformat/package.json
@sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
start: | pre-run ## Starts the React Native packager server
$(call start_packager)
stop: ## Stops the React Native packager server
$(call stop_packager)
check-device-ios:
@if ! [ $(shell which xcodebuild) ]; then \
echo "xcode is not installed"; \
exit 1; \
fi
@if ! [ $(shell which watchman) ]; then \
echo "watchman is not installed"; \
exit 1; \
fi
check-device-android:
@if ! [ $(ANDROID_HOME) ]; then \
echo "ANDROID_HOME is not set"; \
exit 1; \
fi
@if ! [ $(shell which adb 2> /dev/null) ]; then \
echo "adb is not installed"; \
exit 1; \
fi
@echo "Connect your Android device or open the emulator"
@adb wait-for-device
@if ! [ $(shell which watchman 2> /dev/null) ]; then \
echo "watchman is not installed"; \
exit 1; \
fi
prepare-android-build:
@rm -rf ./node_modules/react-native/local-cli/templates/HelloWorld
@rm -rf ./node_modules/react-native-linear-gradient/Examples/
@rm -rf ./node_modules/react-native-orientation/demo/
run: run-ios ## alias for run-ios
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 \
echo Starting React Native packager server; \
npm start & echo Running iOS app in development; \
if [ ! -z "${SIMULATOR}" ]; then \
react-native run-ios --simulator="${SIMULATOR}"; \
else \
react-native run-ios; \
fi; \
wait; \
else \
echo Running iOS app in development; \
if [ ! -z "${SIMULATOR}" ]; then \
react-native run-ios --simulator="${SIMULATOR}"; \
else \
react-native run-ios; \
fi; \
fi
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 \
echo Starting React Native packager server; \
npm start & echo Running Android app in development; \
if [ ! -z ${VARIANT} ]; then \
react-native run-android --no-packager --variant=${VARIANT}; \
else \
react-native run-android --no-packager; \
fi; \
wait; \
else \
echo Running Android app in development; \
if [ ! -z ${VARIANT} ]; then \
react-native run-android --no-packager --variant=${VARIANT}; \
else \
react-native run-android --no-packager; \
fi; \
fi
build: | stop pre-build check-style ## Builds the app for Android & iOS
$(call start_packager)
@echo "Building App"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build
$(call stop_packager)
build-ios: | stop pre-build check-style ## Builds the iOS app
$(call start_packager)
@echo "Building iOS app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios build
$(call stop_packager)
build-android: | stop pre-build check-style prepare-android-build ## Build the Android app
$(call start_packager)
@echo "Building Android app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android build
$(call stop_packager)
unsigned-ios: stop pre-build check-style ## Build an unsigned version of the iOS app
$(call start_packager)
@cd fastlane && NODE_ENV=production bundle exec fastlane ios unsigned
$(call stop_packager)
ios-sim-x86_64: stop pre-build check-style ## Build an unsigned x86_64 version of the iOS app for iPhone simulator
$(call start_packager)
@echo "Building unsigned x86_64 iOS app for iPhone simulator"
@cd fastlane && NODE_ENV=production bundle exec fastlane ios simulator
$(call stop_packager)
unsigned-android: stop pre-build check-style prepare-android-build ## Build an unsigned version of the Android app
@cd fastlane && NODE_ENV=production bundle exec fastlane android unsigned
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
$(call start_packager)
@echo "Building App from PR ${PR_ID}"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build_pr pr:PR-${PR_ID}
$(call stop_packager)
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}'
define start_packager
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
else \
echo React Native packager server already running; \
fi
endef
define stop_packager
@echo Stopping React Native packager server
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 1 ]; then \
ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9; \
echo React Native packager server stopped; \
else \
echo No React Native packager server running; \
fi
endef

View File

@@ -148,29 +148,6 @@ SOFTWARE.
---
## @react-native-community/datetimepicker
This product contains '@react-native-community/datetimepicker' by React Native Community.
React Native date & time picker component for iOS, Android and Windows.
* HOMEPAGE:
* https://github.com/react-native-datetimepicker/datetimepicker
* LICENSE: MIT
MIT License
Copyright (c) 2019 React Native Community
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## @react-native-community/masked-view
This product contains '@react-native-community/masked-view' by React Native Community.
@@ -532,29 +509,6 @@ SOFTWARE.
---
## @types/redux-mock-store
This product contains '@types/redux-mock-store' by Redux.
A mock store for testing Redux async action creators and middleware. The mock store will create an array of dispatched actions which serve as an action log for tests.
* HOMEPAGE:
* https://github.com/reduxjs/redux-mock-store
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2017 Arnaud Benard
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## analytics-react-native
This product contains a modified version of 'analytics-react-native' by Segment.
@@ -576,66 +530,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
---
## array.prototype.flat
This product contains a modified version of 'array.prototype.flat' by ECMAScript Shims.
An ES2019 spec-compliant Array.prototype.flat shim/polyfill/replacement that works as far down as ES3.
* HOMEPAGE:
* https://github.com/es-shims/Array.prototype.flat
* LICENSE: MIT
MIT License
Copyright (c) 2017 ECMAScript Shims
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## base-64
This product contains a modified version of 'base-64' by Dan Kogai.
Yet another Base64 transcoder.
* HOMEPAGE:
* https://github.com/dankogai/js-base64
* LICENSE: BSD 3-Clause "New" or "Revised" License
Copyright (c) 2014, Dan Kogai All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
Neither the name of {{{project}}} nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---
## commonmark
This product contains a modified version of 'commonmark' by John MacFarlane.
@@ -800,6 +694,37 @@ SOFTWARE.
---
## core-js
This product contains 'core-js' by Denis Pushkarev.
Modular standard library for JavaScript.
* HOMEPAGE:
* https://github.com/zloirock/core-js
* LICENSE: Copyright (c) 2014-2019 Denis Pushkarev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
## deep-equal
This product contains 'deep-equal' by James Halliday.
@@ -1596,6 +1521,47 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## react-native-circular-progress
This product contains 'react-native-circular-progress' by Bart Gryszko.
React Native component for creating animated, circular progress with react-native-svg
* HOMEPAGE:
* https://github.com/bgryszko/react-native-circular-progress
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2015 Bart Gryszko
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
## react-native-cookies
This product contains a modified version of 'react-native-cookies' by Joseph P. Ferraro.
@@ -1930,6 +1896,30 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
---
## react-native-image-gallery
This product contains a modified version of 'react-native-image-gallery' by Archriss.
Pure JavaScript image gallery component for iOS and Android
* HOMEPAGE:
* https://github.com/archriss/react-native-image-gallery#readme
* LICENSE: ISC
Note: An original license file for this dependency is not available. We determined the type of license based on the package registry entry for this project. The following text has been prepared using a template from the SPDX Workgroup (https://spdx.org) for this type of license.
ISC License:
Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC")
Copyright (c) 1995-2003 by Internet Software Consortium
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---
## react-native-image-picker
This product contains 'react-native-image-picker' by Marc Shilling.
@@ -2364,39 +2354,6 @@ SOFTWARE.
---
## react-native-redash
This product contains 'react-native-redash' by William Candillon.
The React Native Reanimated and Gesture Handler Toolbelt.
* HOMEPAGE:
* https://github.com/wcandillon/react-native-redash
* LICENSE: MIT License
Copyright (c) 2020 William Candillon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-safe-area
This product contains 'react-native-safe-area' by Masayuki Iwai.
@@ -2525,39 +2482,6 @@ limitations under the License.
---
## react-native-share
This product contains 'react-native-share' by react-native-share.
React Native Share, a simple tool for share message and file to other apps.
* HOMEPAGE:
* https://github.com/react-native-share/react-native-share
* LICENSE: The MIT License (MIT)
Copyright (c) 2015 Esteban Fuentealba
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-slider
This product contains 'react-native-slider' by Jean Regisser.
@@ -2593,38 +2517,28 @@ SOFTWARE.
---
## react-native-startup-time
## react-native-status-bar-size
This product contains 'react-native-startup-time' by doomsower.
This product contains 'react-native-status-bar-size' by Brent Vatne.
This module helps you to measure your app launch time.
Watch and respond to changes in the iOS status bar height
* HOMEPAGE:
* https://github.com/doomsower/react-native-startup-time
* https://github.com/jgkim/react-native-status-bar-size#readme
* LICENSE: MIT
The MIT License (MIT)
Note: An original license file for this dependency is not available. We determined the type of license based on the package registry entry for this project. The following text has been prepared using a template from the SPDX Workgroup (https://spdx.org) for this type of license.
Copyright (c) 2019 Konstantin Kuznetsov
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Copyright (c) 2019 Brent Vatne
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---

View File

@@ -1,6 +1,6 @@
# Mattermost Mobile
- **Minimum Server versions:** Current ESR version (5.37.0)
- **Minimum Server versions:** Current ESR version (5.19)
- **Supported iOS versions:** 11+
- **Supported Android versions:** 7.0+
@@ -63,7 +63,7 @@ We plan to add support for tablets in the future, but the timeline depends on ho
### I keep getting a message "Cannot connect to the server. Please check your server URL and internet connection."
This sometimes appears when there is an issue with the SSL certificate configuration.
This sometimes appears when there is an issue with the SSL certitificate configuration.
To check that your SSL certificate is set up correctly, test the SSL certificate by visiting a site such as https://www.ssllabs.com/ssltest/index.html. If theres an error about the missing chain or certificate path, there is likely an intermediate certificate missing that needs to be included.

View File

@@ -101,7 +101,7 @@ if (System.getenv("SENTRY_ENABLED") == "true") {
* Upload all the APKs to the Play Store and people will download
* the correct one based on the CPU architecture of their device.
*/
def enableSeparateBuildPerCPUArchitecture = project.hasProperty('separateApk') ? project.property('separateApk').toBoolean() : false
def enableSeparateBuildPerCPUArchitecture = false
/**
* Run Proguard to shrink the Java bytecode in release builds.
@@ -122,14 +122,22 @@ def enableHermes = project.ext.react.get("enableHermes", false);
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
versionCode 370
versionName "1.47.0"
versionCode 330
versionName "1.36.0"
multiDexEnabled = true
ndk {
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
}
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
@@ -147,7 +155,7 @@ android {
abi {
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk enableSeparateBuildPerCPUArchitecture // If true, also generate a universal APK
universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}
}
@@ -176,7 +184,7 @@ android {
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
versionCodes.get(abi) * 1000000 + defaultConfig.versionCode
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
}
}
}
@@ -186,10 +194,6 @@ android {
targetCompatibility 1.8
}
packagingOptions {
pickFirst '**/*.so'
}
}
repositories {
@@ -249,7 +253,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation project(':reactnativenotifications')
implementation "com.google.firebase:firebase-messaging:$firebaseVersion"
implementation 'com.google.firebase:firebase-messaging:17.3.4'
// For animated GIF support
implementation 'com.facebook.fresco:fresco:2.0.0'
@@ -264,7 +268,7 @@ dependencies {
// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
from configurations.implementation
from configurations.compile
into 'libs'
}

View File

@@ -4,10 +4,5 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application
android:usesCleartextTraffic="true"
tools:targetApi="28"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" />
</manifest>

View File

@@ -19,20 +19,19 @@
android:theme="@style/AppTheme"
android:installLocation="auto"
android:networkSecurityConfig="@xml/network_security_config"
android:resizeableActivity="true"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true"
>
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
<meta-data android:name="android.content.APP_RESTRICTIONS"
android:resource="@xml/app_restrictions" />
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="184930218130\"/>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTask"
android:taskAffinity="">
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -44,13 +43,8 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mattermost" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mmauthbeta" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<service android:name=".NotificationDismissService"
android:enabled="true"
android:exported="false" />

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,21 +0,0 @@
package com.mattermost.rnbeta;
import com.facebook.react.bridge.JSIModulePackage;
import com.facebook.react.bridge.JSIModuleSpec;
import com.facebook.react.bridge.JavaScriptContextHolder;
import com.facebook.react.bridge.ReactApplicationContext;
import java.util.Collections;
import java.util.List;
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
import com.ammarahmed.mmkv.RNMMKVModule;
public class CustomMMKVJSIModulePackage extends ReanimatedJSIModulePackage {
@Override
public List<JSIModuleSpec> getJSIModules(ReactApplicationContext reactApplicationContext, JavaScriptContextHolder jsContext) {
super.getJSIModules(reactApplicationContext, jsContext);
reactApplicationContext.getNativeModule(RNMMKVModule.class).installLib(jsContext, reactApplicationContext.getFilesDir().getAbsolutePath() + "/mmkv");
return Collections.emptyList();
}
}

View File

@@ -1,144 +1,114 @@
package com.mattermost.rnbeta;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Person;
import android.app.Person.Builder;
import android.app.RemoteInput;
import android.content.Intent;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ApplicationInfo;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.AudioManager;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.os.Build;
import android.provider.Settings.System;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import android.util.Log;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.wix.reactnativenotifications.core.notification.PushNotification;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
import com.mattermost.react_native_interface.ResolvePromise;
import org.json.JSONArray;
import org.json.JSONObject;
import com.facebook.react.bridge.WritableMap;
public class CustomPushNotification extends PushNotification {
private static final String PUSH_NOTIFICATIONS = "PUSH_NOTIFICATIONS";
private static final String VERSION_PREFERENCE = "VERSION_PREFERENCE";
public static final int MESSAGE_NOTIFICATION_ID = 435345;
public static final String GROUP_KEY_MESSAGES = "mm_group_key_messages";
public static final String NOTIFICATION_ID = "notificationId";
public static final String KEY_TEXT_REPLY = "CAN_REPLY";
public static final String NOTIFICATION_REPLIED_EVENT_NAME = "notificationReplied";
private static final String PUSH_TYPE_MESSAGE = "message";
private static final String PUSH_TYPE_CLEAR = "clear";
private static final String PUSH_TYPE_SESSION = "session";
private static final String NOTIFICATIONS_IN_CHANNEL = "notificationsInChannel";
private static final String PUSH_TYPE_UPDATE_BADGE = "update_badge";
private NotificationChannel mHighImportanceChannel;
private NotificationChannel mMinImportanceChannel;
private static Map<String, Integer> channelIdToNotificationCount = new HashMap<String, Integer>();
private static Map<String, List<Bundle>> channelIdToNotification = new HashMap<String, List<Bundle>>();
private static AppLifecycleFacade lifecycleFacade;
private static Context context;
private static int badgeCount = 0;
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
CustomPushNotificationHelper.createNotificationChannels(context);
try {
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
String version = String.valueOf(pInfo.versionCode);
String storedVersion = null;
SharedPreferences pSharedPref = context.getSharedPreferences(VERSION_PREFERENCE, Context.MODE_PRIVATE);
if (pSharedPref != null) {
storedVersion = pSharedPref.getString("Version", "");
}
if (!version.equals(storedVersion)) {
if (pSharedPref != null) {
SharedPreferences.Editor editor = pSharedPref.edit();
editor.putString("Version", version);
editor.apply();
}
Map<String, List<Integer>> inputMap = new HashMap<>();
saveNotificationsMap(context, inputMap);
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
this.context = context;
createNotificationChannels();
}
public static void cancelNotification(Context context, String channelId, Integer notificationId) {
if (!android.text.TextUtils.isEmpty(channelId)) {
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
List<Integer> notifications = notificationsInChannel.get(channelId);
if (notifications == null) {
return;
public static void clearNotification(Context mContext, int notificationId, String channelId) {
if (notificationId != -1) {
Integer count = channelIdToNotificationCount.get(channelId);
if (count == null) {
count = -1;
}
final NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
notificationManager.cancel(notificationId);
notifications.remove(notificationId);
final StatusBarNotification[] statusNotifications = notificationManager.getActiveNotifications();
boolean hasMore = false;
for (final StatusBarNotification status : statusNotifications) {
if (status.getNotification().extras.getString("channel_id").equals(channelId)) {
hasMore = true;
break;
}
}
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
if (!hasMore) {
notificationsInChannel.remove(channelId);
}
saveNotificationsMap(context, notificationsInChannel);
}
}
public static void clearChannelNotifications(Context context, String channelId) {
if (!android.text.TextUtils.isEmpty(channelId)) {
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
List<Integer> notifications = notificationsInChannel.get(channelId);
if (notifications == null) {
return;
}
notificationsInChannel.remove(channelId);
saveNotificationsMap(context, notificationsInChannel);
for (final Integer notificationId : notifications) {
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
if (mContext != null) {
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
if (count != -1) {
int total = CustomPushNotification.badgeCount - count;
int badgeCount = total < 0 ? 0 : total;
CustomPushNotification.badgeCount = badgeCount;
}
}
}
}
public static void clearAllNotifications(Context context) {
if (context != null) {
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
notificationsInChannel.clear();
saveNotificationsMap(context, notificationsInChannel);
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
public static void clearAllNotifications(Context mContext) {
channelIdToNotificationCount.clear();
channelIdToNotification.clear();
if (mContext != null) {
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancelAll();
}
}
@Override
public void onReceived() {
public void onReceived() throws InvalidNotificationException {
final Bundle initialData = mNotificationProps.asBundle();
final String type = initialData.getString("type");
final String ackId = initialData.getString("ack_id");
final String postId = initialData.getString("post_id");
final String channelId = initialData.getString("channel_id");
final boolean isIdLoaded = initialData.getString("id_loaded") != null && initialData.getString("id_loaded").equals("true");
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
if (postId != null) {
notificationId = postId.hashCode();
} else if (channelId != null) {
notificationId = channelId.hashCode();
}
final boolean isIdLoaded = initialData.getString("id_loaded") != null ? initialData.getString("id_loaded").equals("true") : false;
int notificationId = MESSAGE_NOTIFICATION_ID;
if (ackId != null) {
notificationReceiptDelivery(ackId, postId, type, isIdLoaded, new ResolvePromise() {
@@ -157,41 +127,49 @@ public class CustomPushNotification extends PushNotification {
});
}
switch (type) {
case PUSH_TYPE_MESSAGE:
case PUSH_TYPE_SESSION:
if (!mAppLifecycleFacade.isAppVisible()) {
boolean createSummary = type.equals(PUSH_TYPE_MESSAGE);
// notificationReceiptDelivery can override mNotificationProps
// so we fetch the bundle again
final Bundle data = mNotificationProps.asBundle();
if (type.equals(PUSH_TYPE_MESSAGE)) {
if (channelId != null) {
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(mContext);
List<Integer> list = notificationsInChannel.get(channelId);
if (list == null) {
list = Collections.synchronizedList(new ArrayList(0));
}
if (channelId != null) {
notificationId = channelId.hashCode();
list.add(0, notificationId);
if (list.size() > 1) {
createSummary = false;
}
if (createSummary) {
// Add the summary notification id as well
list.add(0, notificationId + 1);
}
notificationsInChannel.put(channelId, list);
saveNotificationsMap(mContext, notificationsInChannel);
}
}
buildNotification(notificationId, createSummary);
synchronized (channelIdToNotificationCount) {
Integer count = channelIdToNotificationCount.get(channelId);
if (count == null) {
count = 0;
}
break;
case PUSH_TYPE_CLEAR:
clearChannelNotifications(mContext, channelId);
break;
count += 1;
channelIdToNotificationCount.put(channelId, count);
}
synchronized (channelIdToNotification) {
List<Bundle> list = channelIdToNotification.get(channelId);
if (list == null) {
list = Collections.synchronizedList(new ArrayList(0));
}
if (PUSH_TYPE_MESSAGE.equals(type)) {
String senderName = getSenderName(data);
data.putLong("time", new Date().getTime());
data.putString("sender_name", senderName);
data.putString("sender_id", data.getString("sender_id"));
}
list.add(0, data);
channelIdToNotification.put(channelId, list);
}
}
switch(type) {
case PUSH_TYPE_MESSAGE:
case PUSH_TYPE_SESSION:
super.postNotification(notificationId);
break;
case PUSH_TYPE_CLEAR:
cancelNotification(data, notificationId);
break;
}
if (mAppLifecycleFacade.isReactInitialized()) {
@@ -201,91 +179,387 @@ public class CustomPushNotification extends PushNotification {
@Override
public void onOpened() {
digestNotification();
Bundle data = mNotificationProps.asBundle();
final String channelId = data.getString("channel_id");
final String postId = data.getString("post_id");
Integer notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
if (postId != null) {
notificationId = postId.hashCode();
}
if (channelId != null) {
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(mContext);
List<Integer> notifications = notificationsInChannel.get(channelId);
notifications.remove(notificationId);
saveNotificationsMap(mContext, notificationsInChannel);
clearChannelNotifications(mContext, channelId);
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
}
}
private void buildNotification(Integer notificationId, boolean createSummary) {
final PendingIntent pendingIntent = super.getCTAPendingIntent();
final Notification notification = buildNotification(pendingIntent);
if (createSummary) {
final Notification summary = getNotificationSummaryBuilder(pendingIntent).build();
super.postNotification(summary, notificationId + 1);
}
super.postNotification(notification, notificationId);
digestNotification();
}
@Override
protected NotificationCompat.Builder getNotificationBuilder(PendingIntent intent) {
protected Notification.Builder getNotificationBuilder(PendingIntent intent) {
// First, get a builder initialized with defaults from the core class.
final Notification.Builder notification = new Notification.Builder(mContext);
Bundle bundle = mNotificationProps.asBundle();
return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, false);
addNotificationExtras(notification, bundle);
setNotificationIcons(notification, bundle);
setNotificationMessagingStyle(notification, bundle);
setNotificationChannel(notification, bundle);
setNotificationBadgeIconType(notification);
NotificationPreferences notificationPreferences = NotificationPreferences.getInstance(mContext);
setNotificationSound(notification, notificationPreferences);
setNotificationVibrate(notification, notificationPreferences);
setNotificationBlink(notification, notificationPreferences);
String channelId = bundle.getString("channel_id");
int notificationId = channelId != null ? channelId.hashCode() : MESSAGE_NOTIFICATION_ID;
setNotificationNumber(notification, channelId);
setNotificationDeleteIntent(notification, notificationId);
addNotificationReplyAction(notification, notificationId, bundle);
notification
.setContentIntent(intent)
.setVisibility(Notification.VISIBILITY_PRIVATE)
.setPriority(Notification.PRIORITY_HIGH)
.setAutoCancel(true);
return notification;
}
protected NotificationCompat.Builder getNotificationSummaryBuilder(PendingIntent intent) {
Bundle bundle = mNotificationProps.asBundle();
return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, true);
private void addNotificationExtras(Notification.Builder notification, Bundle bundle) {
Bundle userInfoBundle = bundle.getBundle("userInfo");
if (userInfoBundle == null) {
userInfoBundle = new Bundle();
}
String channelId = bundle.getString("channel_id");
if (channelId != null) {
userInfoBundle.putString("channel_id", channelId);
}
notification.addExtras(userInfoBundle);
}
private void notificationReceiptDelivery(String ackId, String postId, String type, boolean isIdLoaded, ResolvePromise promise) {
ReceiptDelivery.send(mContext, ackId, postId, type, isIdLoaded, promise);
private void setNotificationIcons(Notification.Builder notification, Bundle bundle) {
String smallIcon = bundle.getString("smallIcon");
String largeIcon = bundle.getString("largeIcon");
int smallIconResId = getSmallIconResourceId(smallIcon);
notification.setSmallIcon(smallIconResId);
int largeIconResId = getLargeIconResourceId(largeIcon);
final Resources res = mContext.getResources();
Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
if (largeIconResId != 0 && (largeIconBitmap != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) {
notification.setLargeIcon(largeIconBitmap);
}
}
private int getSmallIconResourceId(String iconName) {
if (iconName == null) {
iconName = "ic_notification";
}
int resourceId = getIconResourceId(iconName);
if (resourceId == 0) {
iconName = "ic_launcher";
resourceId = getIconResourceId(iconName);
if (resourceId == 0) {
resourceId = android.R.drawable.ic_dialog_info;
}
}
return resourceId;
}
private int getLargeIconResourceId(String iconName) {
if (iconName == null) {
iconName = "ic_launcher";
}
return getIconResourceId(iconName);
}
private int getIconResourceId(String iconName) {
final Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
String defType = "mipmap";
return res.getIdentifier(iconName, defType, packageName);
}
private void setNotificationNumber(Notification.Builder notification, String channelId) {
Integer number = channelIdToNotificationCount.get(channelId);
if (number == null) {
number = 0;
}
notification.setNumber(number);
}
private void setNotificationMessagingStyle(Notification.Builder notification, Bundle bundle) {
Notification.MessagingStyle messagingStyle = getMessagingStyle(bundle);
notification.setStyle(messagingStyle);
}
private Notification.MessagingStyle getMessagingStyle(Bundle bundle) {
Notification.MessagingStyle messagingStyle;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
messagingStyle = new Notification.MessagingStyle("");
} else {
String senderId = bundle.getString("sender_id");
Person sender = new Person.Builder()
.setKey(senderId)
.setName("")
.build();
messagingStyle = new Notification.MessagingStyle(sender);
}
String conversationTitle = getConversationTitle(bundle);
setMessagingStyleConversationTitle(messagingStyle, conversationTitle, bundle);
addMessagingStyleMessages(messagingStyle, conversationTitle, bundle);
return messagingStyle;
}
private String getConversationTitle(Bundle bundle) {
String title = null;
String version = bundle.getString("version");
if (version != null && version.equals("v2")) {
title = bundle.getString("channel_name");
} else {
title = bundle.getString("title");
}
if (android.text.TextUtils.isEmpty(title)) {
ApplicationInfo appInfo = mContext.getApplicationInfo();
title = mContext.getPackageManager().getApplicationLabel(appInfo).toString();
}
return title;
}
private void setMessagingStyleConversationTitle(Notification.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
String channelName = getConversationTitle(bundle);
String senderName = bundle.getString("sender_name");
if (android.text.TextUtils.isEmpty(senderName)) {
senderName = getSenderName(bundle);
}
if (conversationTitle != null && (!conversationTitle.startsWith("@") || channelName != senderName)) {
messagingStyle.setConversationTitle(conversationTitle);
}
}
private void addMessagingStyleMessages(Notification.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
List<Bundle> bundleList;
String channelId = bundle.getString("channel_id");
List<Bundle> bundleArray = channelIdToNotification.get(channelId);
if (bundleArray != null) {
bundleList = new ArrayList<Bundle>(bundleArray);
} else {
bundleList = new ArrayList<Bundle>();
bundleList.add(bundle);
}
int bundleCount = bundleList.size() - 1;
for (int i = bundleCount; i >= 0; i--) {
Bundle data = bundleList.get(i);
String message = data.getString("message");
String senderId = data.getString("sender_id");
if (senderId == null) {
senderId = "sender_id";
}
Bundle userInfoBundle = data.getBundle("userInfo");
String senderName = getSenderName(data);
if (userInfoBundle != null) {
boolean localPushNotificationTest = userInfoBundle.getBoolean("localTest");
if (localPushNotificationTest) {
senderName = "Test";
}
}
if (conversationTitle == null || !android.text.TextUtils.isEmpty(senderName.trim())) {
message = removeSenderNameFromMessage(message, senderName);
}
long timestamp = data.getLong("time");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
messagingStyle.addMessage(message, timestamp, senderName);
} else {
Person sender = new Person.Builder()
.setKey(senderId)
.setName(senderName)
.build();
messagingStyle.addMessage(message, timestamp, sender);
}
}
}
private void setNotificationChannel(Notification.Builder notification, Bundle bundle) {
// If Android Oreo or above we need to register a channel
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
NotificationChannel notificationChannel = mHighImportanceChannel;
boolean localPushNotificationTest = false;
Bundle userInfoBundle = bundle.getBundle("userInfo");
if (userInfoBundle != null) {
localPushNotificationTest = userInfoBundle.getBoolean("localTest");
}
if (mAppLifecycleFacade.isAppVisible() && !localPushNotificationTest) {
notificationChannel = mMinImportanceChannel;
}
notification.setChannelId(notificationChannel.getId());
}
private void setNotificationBadgeIconType(Notification.Builder notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notification.setBadgeIconType(Notification.BADGE_ICON_SMALL);
}
}
private void setNotificationGroup(Notification.Builder notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
notification
.setGroup(GROUP_KEY_MESSAGES)
.setGroupSummary(true);
}
}
private void setNotificationSound(Notification.Builder notification, NotificationPreferences notificationPreferences) {
String soundUri = notificationPreferences.getNotificationSound();
if (soundUri != null) {
if (soundUri != "none") {
notification.setSound(Uri.parse(soundUri), AudioManager.STREAM_NOTIFICATION);
}
} else {
Uri defaultUri = System.DEFAULT_NOTIFICATION_URI;
notification.setSound(defaultUri, AudioManager.STREAM_NOTIFICATION);
}
}
private void setNotificationVibrate(Notification.Builder notification, NotificationPreferences notificationPreferences) {
boolean vibrate = notificationPreferences.getShouldVibrate();
if (vibrate) {
// Use the system default for vibration
notification.setDefaults(Notification.DEFAULT_VIBRATE);
}
}
private void setNotificationBlink(Notification.Builder notification, NotificationPreferences notificationPreferences) {
boolean blink = notificationPreferences.getShouldBlink();
if (blink) {
notification.setLights(Color.CYAN, 500, 500);
}
}
private void setNotificationDeleteIntent(Notification.Builder notification, int notificationId) {
// Let's add a delete intent when the notification is dismissed
Intent delIntent = new Intent(mContext, NotificationDismissService.class);
delIntent.putExtra(NOTIFICATION_ID, notificationId);
PendingIntent deleteIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, delIntent, mNotificationProps);
notification.setDeleteIntent(deleteIntent);
}
private void addNotificationReplyAction(Notification.Builder notification, int notificationId, Bundle bundle) {
String postId = bundle.getString("post_id");
if (android.text.TextUtils.isEmpty(postId) || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return;
}
Intent replyIntent = new Intent(mContext, NotificationReplyBroadcastReceiver.class);
replyIntent.setAction(KEY_TEXT_REPLY);
replyIntent.putExtra(NOTIFICATION_ID, notificationId);
replyIntent.putExtra("pushNotification", bundle);
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(
mContext,
notificationId,
replyIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
.setLabel("Reply")
.build();
int icon = R.drawable.ic_notif_action_reply;
CharSequence title = "Reply";
Notification.Action replyAction = new Notification.Action.Builder(icon, title, replyPendingIntent)
.addRemoteInput(remoteInput)
.setAllowGeneratedReplies(true)
.build();
notification
.setShowWhen(true)
.addAction(replyAction);
}
private void notifyReceivedToJS() {
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
}
private static void saveNotificationsMap(Context context, Map<String, List<Integer>> inputMap) {
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
if (pSharedPref != null && context != null) {
JSONObject json = new JSONObject(inputMap);
String jsonString = json.toString();
SharedPreferences.Editor editor = pSharedPref.edit();
editor.remove(NOTIFICATIONS_IN_CHANNEL).commit();
editor.putString(NOTIFICATIONS_IN_CHANNEL, jsonString);
editor.commit();
}
private void cancelNotification(Bundle data, int notificationId) {
final String channelId = data.getString("channel_id");
final String badge = data.getString("badge");
CustomPushNotification.badgeCount = Integer.parseInt(badge);
CustomPushNotification.clearNotification(mContext.getApplicationContext(), notificationId, channelId);
}
private static Map<String, List<Integer>> loadNotificationsMap(Context context) {
Map<String, List<Integer>> outputMap = new HashMap<>();
if (context != null) {
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
try {
if (pSharedPref != null) {
String jsonString = pSharedPref.getString(NOTIFICATIONS_IN_CHANNEL, (new JSONObject()).toString());
JSONObject json = new JSONObject(jsonString);
Iterator<String> keysItr = json.keys();
while (keysItr.hasNext()) {
String key = keysItr.next();
JSONArray array = json.getJSONArray(key);
List<Integer> values = new ArrayList<>();
for (int i = 0; i < array.length(); ++i) {
values.add(array.getInt(i));
}
outputMap.put(key, values);
}
}
} catch (Exception e) {
e.printStackTrace();
private String getSenderName(Bundle bundle) {
String senderName = bundle.getString("sender_name");
if (senderName != null) {
return senderName;
}
String channelName = bundle.getString("channel_name");
if (channelName != null && channelName.startsWith("@")) {
return channelName;
}
String message = bundle.getString("message");
if (message != null) {
String name = message.split(":")[0];
if (name != message) {
return name;
}
}
return outputMap;
return getConversationTitle(bundle);
}
private String removeSenderNameFromMessage(String message, String senderName) {
Integer index = message.indexOf(senderName);
if (index == 0) {
message = message.substring(senderName.length());
}
return message.replaceFirst(": ", "").trim();
}
private void notificationReceiptDelivery(String ackId, String postId, String type, boolean isIdLoaded, ResolvePromise promise) {
ReceiptDelivery.send(context, ackId, postId, type, isIdLoaded, promise);
}
private void createNotificationChannels() {
// Notification channels are not supported in Android Nougat and below
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
mHighImportanceChannel = new NotificationChannel("channel_01", "High Importance", NotificationManager.IMPORTANCE_HIGH);
mHighImportanceChannel.setShowBadge(true);
notificationManager.createNotificationChannel(mHighImportanceChannel);
mMinImportanceChannel = new NotificationChannel("channel_02", "Min Importance", NotificationManager.IMPORTANCE_MIN);
mMinImportanceChannel.setShowBadge(true);
notificationManager.createNotificationChannel(mMinImportanceChannel);
}
}

View File

@@ -4,6 +4,10 @@ import android.content.Context;
import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.notificationdrawer.PushNotificationsDrawer;
import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer;
import com.wix.reactnativenotifications.core.notificationdrawer.INotificationsDrawerApplication;
import static com.wix.reactnativenotifications.Defs.LOGTAG;
public class CustomPushNotificationDrawer extends PushNotificationsDrawer {
final protected Context mContext;

View File

@@ -1,457 +0,0 @@
package com.mattermost.rnbeta;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.Person;
import androidx.core.app.RemoteInput;
import androidx.core.graphics.drawable.IconCompat;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableMap;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
import com.wix.reactnativenotifications.core.notification.PushNotificationProps;
import java.io.IOException;
import java.util.Date;
import java.util.Objects;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class CustomPushNotificationHelper {
public static final String CHANNEL_HIGH_IMPORTANCE_ID = "channel_01";
public static final String CHANNEL_MIN_IMPORTANCE_ID = "channel_02";
public static final String KEY_TEXT_REPLY = "CAN_REPLY";
public static final int MESSAGE_NOTIFICATION_ID = 435345;
public static final String NOTIFICATION_ID = "notificationId";
private static NotificationChannel mHighImportanceChannel;
private static NotificationChannel mMinImportanceChannel;
private static void addMessagingStyleMessages(Context context, NotificationCompat.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
String message = bundle.getString("message", bundle.getString("body"));
String senderId = bundle.getString("sender_id");
if (senderId == null) {
senderId = "sender_id";
}
Bundle userInfoBundle = bundle.getBundle("userInfo");
String senderName = getSenderName(bundle);
if (userInfoBundle != null) {
boolean localPushNotificationTest = userInfoBundle.getBoolean("test");
if (localPushNotificationTest) {
senderName = "Test";
}
}
if (conversationTitle == null || !android.text.TextUtils.isEmpty(senderName.trim())) {
message = removeSenderNameFromMessage(message, senderName);
}
long timestamp = new Date().getTime();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
messagingStyle.addMessage(message, timestamp, senderName);
} else {
Person.Builder sender = new Person.Builder()
.setKey(senderId)
.setName(senderName);
try {
Bitmap avatar = userAvatar(context, senderId);
if (avatar != null) {
sender.setIcon(IconCompat.createWithBitmap(avatar));
}
} catch (IOException e) {
e.printStackTrace();
}
messagingStyle.addMessage(message, timestamp, sender.build());
}
}
private static void addNotificationExtras(NotificationCompat.Builder notification, Bundle bundle) {
Bundle userInfoBundle = bundle.getBundle("userInfo");
if (userInfoBundle == null) {
userInfoBundle = new Bundle();
}
String channelId = bundle.getString("channel_id");
if (channelId != null) {
userInfoBundle.putString("channel_id", channelId);
}
notification.addExtras(userInfoBundle);
}
private static void addNotificationReplyAction(Context context, NotificationCompat.Builder notification, Bundle bundle, int notificationId) {
String postId = bundle.getString("post_id");
if (android.text.TextUtils.isEmpty(postId)) {
return;
}
Intent replyIntent = new Intent(context, NotificationReplyBroadcastReceiver.class);
replyIntent.setAction(KEY_TEXT_REPLY);
replyIntent.putExtra(NOTIFICATION_ID, notificationId);
replyIntent.putExtra("pushNotification", bundle);
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(
context,
notificationId,
replyIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
.setLabel("Reply")
.build();
int icon = R.drawable.ic_notif_action_reply;
CharSequence title = "Reply";
NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(icon, title, replyPendingIntent)
.addRemoteInput(remoteInput)
.setAllowGeneratedReplies(true)
.build();
notification
.setShowWhen(true)
.addAction(replyAction);
}
public static NotificationCompat.Builder createNotificationBuilder(Context context, PendingIntent intent, Bundle bundle, boolean createSummary) {
final NotificationCompat.Builder notification = new NotificationCompat.Builder(context, CHANNEL_HIGH_IMPORTANCE_ID);
String channelId = bundle.getString("channel_id");
String postId = bundle.getString("post_id");
int notificationId = postId != null ? postId.hashCode() : MESSAGE_NOTIFICATION_ID;
NotificationPreferences notificationPreferences = NotificationPreferences.getInstance(context);
addNotificationExtras(notification, bundle);
setNotificationIcons(context, notification, bundle);
setNotificationMessagingStyle(context, notification, bundle);
setNotificationGroup(notification, channelId, createSummary);
setNotificationBadgeType(notification);
setNotificationSound(notification, notificationPreferences);
setNotificationVibrate(notification, notificationPreferences);
setNotificationBlink(notification, notificationPreferences);
setNotificationChannel(notification, bundle);
setNotificationDeleteIntent(context, notification, bundle, notificationId);
addNotificationReplyAction(context, notification, bundle, notificationId);
notification
.setContentIntent(intent)
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
.setPriority(Notification.PRIORITY_HIGH)
.setCategory(Notification.CATEGORY_MESSAGE)
.setAutoCancel(true);
return notification;
}
public static void createNotificationChannels(Context context) {
// Notification channels are not supported in Android Nougat and below
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
if (mHighImportanceChannel == null) {
mHighImportanceChannel = new NotificationChannel(CHANNEL_HIGH_IMPORTANCE_ID, "High Importance", NotificationManager.IMPORTANCE_HIGH);
mHighImportanceChannel.setShowBadge(true);
notificationManager.createNotificationChannel(mHighImportanceChannel);
}
if (mMinImportanceChannel == null) {
mMinImportanceChannel = new NotificationChannel(CHANNEL_MIN_IMPORTANCE_ID, "Min Importance", NotificationManager.IMPORTANCE_MIN);
mMinImportanceChannel.setShowBadge(true);
notificationManager.createNotificationChannel(mMinImportanceChannel);
}
}
private static Bitmap getCircleBitmap(Bitmap bitmap) {
final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),
bitmap.getHeight(), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(output);
final int color = Color.RED;
final Paint paint = new Paint();
final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
final RectF rectF = new RectF(rect);
paint.setAntiAlias(true);
canvas.drawARGB(0, 0, 0, 0);
paint.setColor(color);
canvas.drawOval(rectF, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bitmap, rect, rect, paint);
bitmap.recycle();
return output;
}
private static String getConversationTitle(Bundle bundle) {
String title = bundle.getString("channel_name");
if (android.text.TextUtils.isEmpty(title)) {
title = bundle.getString("sender_name");
}
return title;
}
private static int getIconResourceId(Context context, String iconName) {
final Resources res = context.getResources();
String packageName = context.getPackageName();
String defType = "mipmap";
return res.getIdentifier(iconName, defType, packageName);
}
private static NotificationCompat.MessagingStyle getMessagingStyle(Context context, Bundle bundle) {
NotificationCompat.MessagingStyle messagingStyle;
String senderId = "me";
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
messagingStyle = new NotificationCompat.MessagingStyle("Me");
} else {
Person.Builder sender = new Person.Builder()
.setKey(senderId)
.setName("Me");
try {
Bitmap avatar = userAvatar(context, "me");
if (avatar != null) {
sender.setIcon(IconCompat.createWithBitmap(avatar));
}
} catch (IOException e) {
e.printStackTrace();
}
messagingStyle = new NotificationCompat.MessagingStyle(sender.build());
}
String conversationTitle = getConversationTitle(bundle);
setMessagingStyleConversationTitle(messagingStyle, conversationTitle, bundle);
addMessagingStyleMessages(context, messagingStyle, conversationTitle, bundle);
return messagingStyle;
}
private static String getSenderName(Bundle bundle) {
String senderName = bundle.getString("sender_name");
if (senderName != null) {
return senderName;
}
String channelName = bundle.getString("channel_name");
if (channelName != null && channelName.startsWith("@")) {
return channelName;
}
String message = bundle.getString("message");
if (message != null) {
String name = message.split(":")[0];
if (!name.equals(message)) {
return name;
}
}
return getConversationTitle(bundle);
}
public static int getSmallIconResourceId(Context context, String iconName) {
if (iconName == null) {
iconName = "ic_notification";
}
int resourceId = getIconResourceId(context, iconName);
if (resourceId == 0) {
iconName = "ic_launcher";
resourceId = getIconResourceId(context, iconName);
if (resourceId == 0) {
resourceId = android.R.drawable.ic_dialog_info;
}
}
return resourceId;
}
private static String removeSenderNameFromMessage(String message, String senderName) {
int index = message.indexOf(senderName);
if (index == 0) {
message = message.substring(senderName.length());
}
return message.replaceFirst(": ", "").trim();
}
private static void setMessagingStyleConversationTitle(NotificationCompat.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
String channelName = getConversationTitle(bundle);
String senderName = bundle.getString("sender_name");
if (TextUtils.isEmpty(senderName)) {
senderName = getSenderName(bundle);
}
if (conversationTitle != null && !channelName.equals(senderName)) {
messagingStyle.setConversationTitle(conversationTitle);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
messagingStyle.setGroupConversation(true);
}
}
}
private static void setNotificationBadgeType(NotificationCompat.Builder notification) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notification.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE);
}
}
private static void setNotificationBlink(NotificationCompat.Builder notification, NotificationPreferences notificationPreferences) {
boolean blink = notificationPreferences.getShouldBlink();
if (blink) {
notification.setLights(Color.CYAN, 500, 500);
}
}
private static void setNotificationChannel(NotificationCompat.Builder notification, Bundle bundle) {
// If Android Oreo or above we need to register a channel
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
NotificationChannel notificationChannel = mHighImportanceChannel;
boolean testNotification = false;
boolean localNotification = false;
Bundle userInfoBundle = bundle.getBundle("userInfo");
if (userInfoBundle != null) {
testNotification = userInfoBundle.getBoolean("test");
localNotification = userInfoBundle.getBoolean("local");
}
if (testNotification || localNotification) {
notificationChannel = mMinImportanceChannel;
}
notification.setChannelId(notificationChannel.getId());
}
private static void setNotificationDeleteIntent(Context context, NotificationCompat.Builder notification, Bundle bundle, int notificationId) {
// Let's add a delete intent when the notification is dismissed
Intent delIntent = new Intent(context, NotificationDismissService.class);
PushNotificationProps notificationProps = new PushNotificationProps(bundle);
delIntent.putExtra(NOTIFICATION_ID, notificationId);
PendingIntent deleteIntent = NotificationIntentAdapter.createPendingNotificationIntent(context, delIntent, notificationProps);
notification.setDeleteIntent(deleteIntent);
}
private static void setNotificationMessagingStyle(Context context, NotificationCompat.Builder notification, Bundle bundle) {
NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(context, bundle);
notification.setStyle(messagingStyle);
}
private static void setNotificationGroup(NotificationCompat.Builder notification, String channelId, boolean setAsSummary) {
notification.setGroup(channelId);
if (setAsSummary) {
// if this is the first notification for the channel then set as summary, otherwise skip
notification.setGroupSummary(true);
}
}
private static void setNotificationIcons(Context context, NotificationCompat.Builder notification, Bundle bundle) {
String smallIcon = bundle.getString("smallIcon");
String channelName = getConversationTitle(bundle);
String senderName = bundle.getString("sender_name");
int smallIconResId = getSmallIconResourceId(context, smallIcon);
notification.setSmallIcon(smallIconResId);
if (channelName.equals(senderName)) {
try {
String senderId = bundle.getString("sender_id");
Bitmap avatar = userAvatar(context, senderId);
if (avatar != null) {
notification.setLargeIcon(avatar);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static void setNotificationSound(NotificationCompat.Builder notification, NotificationPreferences notificationPreferences) {
String soundUri = notificationPreferences.getNotificationSound();
if (soundUri != null) {
if (!soundUri.equals("none")) {
notification.setSound(Uri.parse(soundUri));
}
} else {
Uri defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI;
notification.setSound(defaultUri);
}
}
private static void setNotificationVibrate(NotificationCompat.Builder notification, NotificationPreferences notificationPreferences) {
boolean vibrate = notificationPreferences.getShouldVibrate();
if (vibrate) {
// Use the system default for vibration
notification.setDefaults(Notification.DEFAULT_VIBRATE);
}
}
private static Bitmap userAvatar(Context context, final String userId) throws IOException {
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
final ReadableMap credentials = MattermostCredentialsHelper.getCredentialsSync(reactApplicationContext);
final String serverUrl = credentials.getString("serverUrl");
final String token = credentials.getString("token");
final OkHttpClient client = new OkHttpClient();
final String url = String.format("%s/api/v4/users/%s/image", serverUrl, userId);
Request request = new Request.Builder()
.header("Authorization", String.format("Bearer %s", token))
.url(url)
.build();
Response response = client.newCall(request).execute();
if (response.code() == 200) {
assert response.body() != null;
byte[] bytes = response.body().bytes();
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
Log.i("ReactNative", String.format("Fetch profile %s", url));
return getCircleBitmap(bitmap);
}
return null;
}
}

View File

@@ -1,8 +1,6 @@
package com.mattermost.rnbeta;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.view.KeyEvent;
import android.content.res.Configuration;
@@ -21,7 +19,7 @@ public class MainActivity extends NavigationActivity {
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) {
@@ -44,7 +42,7 @@ public class MainActivity extends NavigationActivity {
return true;
}
return super.dispatchKeyEvent(event);
}
};
private void setHWKeyboardConnected() {
HWKeyboardConnected = getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY;

View File

@@ -1,9 +1,11 @@
package com.mattermost.rnbeta;
import androidx.annotation.Nullable;
import android.content.Context;
import android.content.RestrictionsManager;
import android.os.Bundle;
import android.util.Log;
import java.lang.reflect.InvocationTargetException;
import java.io.File;
import java.util.HashMap;
import java.util.List;
@@ -28,20 +30,30 @@ import com.facebook.react.ReactPackage;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.TurboReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.bridge.ReactMarkerConstants;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.soloader.SoLoader;
import com.facebook.react.bridge.JSIModulePackage;
public class MainApplication extends NavigationApplication implements INotificationsApplication, INotificationsDrawerApplication {
public static MainApplication instance;
public Boolean sharedExtensionIsOpened = false;
public long APP_START_TIME;
public long RELOAD;
public long CONTENT_APPEARED;
public long PROCESS_PACKAGES_START;
public long PROCESS_PACKAGES_END;
private Bundle mManagedConfig = null;
private final ReactNativeHost mReactNativeHost =
@@ -53,10 +65,12 @@ private final ReactNativeHost mReactNativeHost =
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be auto linked yet can be added manually here, for example:
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new RNNotificationsPackage(MainApplication.this));
packages.add(new RNPasteableTextInputPackage());
packages.add(
new TurboReactPackage() {
@Override
@@ -77,13 +91,16 @@ private final ReactNativeHost mReactNativeHost =
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
return () -> {
Map<String, ReactModuleInfo> map = new HashMap<>();
map.put("MattermostManaged", new ReactModuleInfo("MattermostManaged", "com.mattermost.rnbeta.MattermostManagedModule", false, false, false, false, false));
map.put("MattermostShare", new ReactModuleInfo("MattermostShare", "com.mattermost.share.ShareModule", false, false, true, false, false));
map.put("NotificationPreferences", new ReactModuleInfo("NotificationPreferences", "com.mattermost.rnbeta.NotificationPreferencesModule", false, false, false, false, false));
map.put("RNTextInputReset", new ReactModuleInfo("RNTextInputReset", "com.mattermost.rnbeta.RNTextInputResetModule", false, false, false, false, false));
return map;
return new ReactModuleInfoProvider() {
@Override
public Map<String, ReactModuleInfo> getReactModuleInfos() {
Map<String, ReactModuleInfo> map = new HashMap<>();
map.put("MattermostManaged", new ReactModuleInfo("MattermostManaged", "com.mattermost.rnbeta.MattermostManagedModule", false, false, false, false, false));
map.put("MattermostShare", new ReactModuleInfo("MattermostShare", "com.mattermost.share.ShareModule", false, false, true, false, false));
map.put("NotificationPreferences", new ReactModuleInfo("NotificationPreferences", "com.mattermost.rnbeta.NotificationPreferencesModule", false, false, false, false, false));
map.put("RNTextInputReset", new ReactModuleInfo("RNTextInputReset", "com.mattermost.rnbeta.RNTextInputResetModule", false, false, false, false, false));
return map;
}
};
}
}
@@ -96,11 +113,6 @@ private final ReactNativeHost mReactNativeHost =
protected String getJSMainModuleName() {
return "index";
}
@Override
protected JSIModulePackage getJSIModulePackage() {
return (JSIModulePackage) new CustomMMKVJSIModulePackage();
}
};
@Override
@@ -120,6 +132,9 @@ private final ReactNativeHost mReactNativeHost =
SoLoader.init(this, /* native exopackage */ false);
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
// Uncomment to listen to react markers for build that has telemetry enabled
// addReactMarkerListener();
}
@Override
@@ -154,6 +169,7 @@ private final ReactNativeHost mReactNativeHost =
(RestrictionsManager) ctx.getSystemService(Context.RESTRICTIONS_SERVICE);
mManagedConfig = myRestrictionsMgr.getApplicationRestrictions();
myRestrictionsMgr = null;
if (mManagedConfig!= null && mManagedConfig.size() > 0) {
return mManagedConfig;
@@ -179,12 +195,44 @@ private final ReactNativeHost mReactNativeHost =
return null;
}
private void addReactMarkerListener() {
ReactMarker.addListener(new ReactMarker.MarkerListener() {
@Override
public void logMarker(ReactMarkerConstants name, @Nullable String tag, int instanceKey) {
if (name.toString() == ReactMarkerConstants.RELOAD.toString()) {
APP_START_TIME = System.currentTimeMillis();
RELOAD = System.currentTimeMillis();
} else if (name.toString() == ReactMarkerConstants.PROCESS_PACKAGES_START.toString()) {
PROCESS_PACKAGES_START = System.currentTimeMillis();
} else if (name.toString() == ReactMarkerConstants.PROCESS_PACKAGES_END.toString()) {
PROCESS_PACKAGES_END = System.currentTimeMillis();
} else if (name.toString() == ReactMarkerConstants.CONTENT_APPEARED.toString()) {
CONTENT_APPEARED = System.currentTimeMillis();
ReactContext ctx = getRunningReactContext();
if (ctx != null) {
WritableMap map = Arguments.createMap();
map.putDouble("appReload", RELOAD);
map.putDouble("appContentAppeared", CONTENT_APPEARED);
map.putDouble("processPackagesStart", PROCESS_PACKAGES_START);
map.putDouble("processPackagesEnd", PROCESS_PACKAGES_END);
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).
emit("nativeMetrics", map);
}
}
}
});
}
/**
* Loads Flipper in React Native templates. Call this in the onCreate method with something like
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
*
* @param context application context
* @param reactInstanceManager instance of React
* @param context
* @param reactInstanceManager
*/
private static void initializeFlipper(
Context context, ReactInstanceManager reactInstanceManager) {
@@ -194,11 +242,17 @@ private final ReactNativeHost mReactNativeHost =
We use reflection here to pick up the class that initializes Flipper,
since Flipper library is not available in release mode
*/
Class<?> aClass = Class.forName("com.rn.ReactNativeFlipper");
Class<?> aClass = Class.forName("com.rndiffapp.ReactNativeFlipper");
aClass
.getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
.invoke(null, context, reactInstanceManager);
} catch (Exception e) {
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}

View File

@@ -1,14 +1,12 @@
package com.mattermost.rnbeta;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.content.Context;
import java.util.ArrayList;
import java.util.HashMap;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.oblador.keychain.KeychainModule;
@@ -18,17 +16,12 @@ import com.mattermost.react_native_interface.KeysReadableArray;
public class MattermostCredentialsHelper {
static final String CURRENT_SERVER_URL = "@currentServerUrl";
static KeychainModule keychainModule;
public static void getCredentialsForCurrentServer(ReactApplicationContext context, ResolvePromise promise) {
final ArrayList<String> keys = new ArrayList<>(1);
final KeychainModule keychainModule = new KeychainModule(context);
final AsyncStorageHelper asyncStorage = new AsyncStorageHelper(context);
final ArrayList<String> keys = new ArrayList<String>(1);
keys.add(CURRENT_SERVER_URL);
if (keychainModule == null) {
keychainModule = new KeychainModule(context);
}
AsyncStorageHelper asyncStorage = new AsyncStorageHelper(context);
KeysReadableArray asyncStorageKeys = new KeysReadableArray() {
@Override
public int size() {
@@ -36,7 +29,6 @@ public class MattermostCredentialsHelper {
}
@Override
@NonNull
public String getString(int index) {
return keys.get(index);
}
@@ -44,42 +36,7 @@ public class MattermostCredentialsHelper {
HashMap<String, String> asyncStorageResults = asyncStorage.multiGet(asyncStorageKeys);
String serverUrl = asyncStorageResults.get(CURRENT_SERVER_URL);
final WritableMap options = Arguments.createMap();
// KeyChain module fails if `authenticationPrompt` is not set
final WritableMap authPrompt = Arguments.createMap();
authPrompt.putString("title", "Authenticate to retrieve secret");
authPrompt.putString("cancel", "Cancel");
options.putMap("authenticationPrompt", authPrompt);
options.putString("service", serverUrl);
keychainModule.getGenericPasswordForOptions(options, promise);
}
public static ReadableMap getCredentialsSync(ReactApplicationContext context) {
final String[] serverUrl = new String[1];
final String[] token = new String[1];
MattermostCredentialsHelper.getCredentialsForCurrentServer(context, new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
WritableMap map = (WritableMap) value;
if (map != null) {
token[0] = map.getString("password");
serverUrl[0] = map.getString("service");
assert serverUrl[0] != null;
if (serverUrl[0].isEmpty()) {
String[] credentials = token[0].split(",[ ]*");
if (credentials.length == 2) {
token[0] = credentials[0];
serverUrl[0] = credentials[1];
}
}
}
}
});
final WritableMap result = Arguments.createMap();
result.putString("serverUrl", serverUrl[0]);
result.putString("token", token[0]);
return result;
keychainModule.getGenericPasswordForOptions(serverUrl, promise);
}
}

View File

@@ -1,6 +1,7 @@
package com.mattermost.rnbeta;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -11,12 +12,10 @@ import android.view.WindowManager.LayoutParams;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.NonNull;
import java.util.Objects;
import java.util.Set;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
@@ -66,7 +65,6 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule implemen
}
@Override
@NonNull
public String getName() {
return "MattermostManaged";
}
@@ -94,7 +92,7 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule implemen
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getReactApplicationContext().startActivity(intent);
Objects.requireNonNull(getCurrentActivity()).finish();
getCurrentActivity().finish();
System.exit(0);
}
@@ -113,7 +111,7 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule implemen
@ReactMethod
public void quitApp() {
Objects.requireNonNull(getCurrentActivity()).finish();
getCurrentActivity().finish();
System.exit(0);
}
@@ -165,7 +163,6 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule implemen
blurAppScreen = Boolean.parseBoolean(config.getString("blurApplicationScreen"));
}
assert activity != null;
if (blurAppScreen) {
activity.getWindow().setFlags(LayoutParams.FLAG_SECURE, LayoutParams.FLAG_SECURE);
} else {
@@ -197,7 +194,7 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule implemen
if(one.size() != two.size())
return false;
Set<String> setOne = new ArraySet<>();
Set<String> setOne = new ArraySet<String>();
setOne.addAll(one.keySet());
setOne.addAll(two.keySet());
Object valueOne;

View File

@@ -9,24 +9,18 @@ import android.util.Log;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationDismissService extends IntentService {
private Context mContext;
public NotificationDismissService() {
super("notificationDismissService");
}
@Override
protected void onHandleIntent(Intent intent) {
final Context context = getApplicationContext();
final Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
final String channelId = bundle.getString("channel_id");
final String postId = bundle.getString("post_id");
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
if (postId != null) {
notificationId = postId.hashCode();
} else if (channelId != null) {
notificationId = channelId.hashCode();
}
CustomPushNotification.cancelNotification(context, channelId, notificationId);
mContext = getApplicationContext();
Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
String channelId = bundle.getString("channel_id");
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
Log.i("ReactNative", "Dismiss notification");
}
}

View File

@@ -10,7 +10,8 @@ public class NotificationPreferences {
public final String SOUND_PREF = "NotificationSound";
public final String VIBRATE_PREF = "NotificationVibrate";
public final String BLINK_PREF = "NotificationLights";
private final SharedPreferences mSharedPreferences;
private SharedPreferences mSharedPreferences;
private NotificationPreferences(Context context) {
mSharedPreferences = context.getSharedPreferences(SHARED_NAME, Context.MODE_PRIVATE);
@@ -39,18 +40,18 @@ public class NotificationPreferences {
public void setNotificationSound(String soundUri) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putString(SOUND_PREF, soundUri);
editor.apply();
editor.commit();
}
public void setShouldVibrate(boolean vibrate) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putBoolean(VIBRATE_PREF, vibrate);
editor.apply();
editor.commit();
}
public void setShouldBlink(boolean blink) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putBoolean(BLINK_PREF, blink);
editor.apply();
editor.commit();
}
}

View File

@@ -1,5 +1,6 @@
package com.mattermost.rnbeta;
import android.app.Application;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
@@ -10,10 +11,10 @@ import android.os.Bundle;
import android.net.Uri;
import android.service.notification.StatusBarNotification;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
@@ -23,12 +24,13 @@ import com.facebook.react.bridge.WritableMap;
public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
private static NotificationPreferencesModule instance;
private final MainApplication mApplication;
private final NotificationPreferences mNotificationPreference;
private NotificationPreferences mNotificationPreference;
private NotificationPreferencesModule(MainApplication application, ReactApplicationContext reactContext) {
super(reactContext);
mApplication = application;
mNotificationPreference = NotificationPreferences.getInstance(reactContext);
Context context = mApplication.getApplicationContext();
mNotificationPreference = NotificationPreferences.getInstance(context);
}
public static NotificationPreferencesModule getInstance(MainApplication application, ReactApplicationContext reactContext) {
@@ -44,7 +46,6 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
}
@Override
@NonNull
public String getName() {
return "NotificationPreferences";
}
@@ -52,7 +53,7 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
@ReactMethod
public void getPreferences(final Promise promise) {
try {
final Context context = mApplication.getApplicationContext();
Context context = mApplication.getApplicationContext();
RingtoneManager manager = new RingtoneManager(context);
manager.setType(RingtoneManager.TYPE_NOTIFICATION);
Cursor cursor = manager.getCursor();
@@ -87,7 +88,7 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
@ReactMethod
public void previewSound(String url) {
final Context context = mApplication.getApplicationContext();
Context context = mApplication.getApplicationContext();
Uri uri = Uri.parse(url);
Ringtone r = RingtoneManager.getRingtone(context, uri);
r.play();
@@ -110,7 +111,7 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
@ReactMethod
public void getDeliveredNotifications(final Promise promise) {
final Context context = mApplication.getApplicationContext();
Context context = mApplication.getApplicationContext();
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
StatusBarNotification[] statusBarNotifications = notificationManager.getActiveNotifications();
WritableArray result = Arguments.createArray();
@@ -118,7 +119,9 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
WritableMap map = Arguments.createMap();
Notification n = sbn.getNotification();
Bundle bundle = n.extras;
int identifier = sbn.getId();
String channelId = bundle.getString("channel_id");
map.putInt("identifier", identifier);
map.putString("channel_id", channelId);
result.pushMap(map);
}
@@ -126,8 +129,8 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
}
@ReactMethod
public void removeDeliveredNotifications(String channelId) {
final Context context = mApplication.getApplicationContext();
CustomPushNotification.clearChannelNotifications(context, channelId);
public void removeDeliveredNotifications(int identifier, String channelId) {
Context context = mApplication.getApplicationContext();
CustomPushNotification.clearNotification(context, identifier, channelId);
}
}

View File

@@ -1,21 +1,18 @@
package com.mattermost.rnbeta;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Nullable;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.Person;
import java.io.IOException;
import java.util.Objects;
import okhttp3.Call;
import okhttp3.MediaType;
@@ -27,17 +24,16 @@ import okhttp3.Response;
import org.json.JSONObject;
import org.json.JSONException;
import com.facebook.react.bridge.ReadableMap;
import com.mattermost.react_native_interface.ResolvePromise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableMap;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
import com.wix.reactnativenotifications.core.ProxyService;
import com.wix.reactnativenotifications.core.notification.PushNotificationProps;
public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
private Context mContext;
private Bundle bundle;
private NotificationManagerCompat notificationManager;
private NotificationManager notificationManager;
@Override
public void onReceive(Context context, Intent intent) {
@@ -49,13 +45,28 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
mContext = context;
bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
notificationManager = NotificationManagerCompat.from(context);
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
final int notificationId = intent.getIntExtra(CustomPushNotificationHelper.NOTIFICATION_ID, -1);
final int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
ReadableMap results = MattermostCredentialsHelper.getCredentialsSync(reactApplicationContext);
replyToMessage(results.getString("serverUrl"), results.getString("token"), notificationId, message);
MattermostCredentialsHelper.getCredentialsForCurrentServer(reactApplicationContext, new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
if (value instanceof Boolean && !(Boolean)value) {
return;
}
WritableMap map = (WritableMap) value;
if (map != null) {
String token = map.getString("password");
String serverUrl = map.getString("service");
replyToMessage(serverUrl, token, notificationId, message);
}
}
});
}
}
@@ -68,7 +79,7 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
}
if (token == null || serverUrl == null) {
onReplyFailed(notificationId);
onReplyFailed(notificationManager, notificationId, channelId);
return;
}
@@ -89,20 +100,19 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
client.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
public void onFailure(Call call, IOException e) {
Log.i("ReactNative", String.format("Reply FAILED exception %s", e.getMessage()));
onReplyFailed(notificationId);
onReplyFailed(notificationManager, notificationId, channelId);
}
@Override
public void onResponse(@NonNull Call call, @NonNull final Response response) throws IOException {
public void onResponse(Call call, final Response response) throws IOException {
if (response.isSuccessful()) {
onReplySuccess(notificationId, message);
onReplySuccess(notificationManager, notificationId, channelId);
Log.i("ReactNative", "Reply SUCCESS");
} else {
assert response.body() != null;
Log.i("ReactNative", String.format("Reply FAILED status %s BODY %s", response.code(), Objects.requireNonNull(response.body()).string()));
onReplyFailed(notificationId);
Log.i("ReactNative", String.format("Reply FAILED status %s BODY %s", response.code(), response.body().string()));
onReplyFailed(notificationManager, notificationId, channelId);
}
}
});
@@ -120,31 +130,37 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
}
}
protected void onReplyFailed(int notificationId) {
recreateNotification(notificationId, "Message failed to send.");
}
protected void onReplyFailed(NotificationManager notificationManager, int notificationId, String channelId) {
String CHANNEL_ID = "Reply job";
Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
protected void onReplySuccess(int notificationId, final CharSequence message) {
recreateNotification(notificationId, message);
}
Bundle userInfoBundle = new Bundle();
userInfoBundle.putString("channel_id", channelId);
private void recreateNotification(int notificationId, final CharSequence message) {
final Intent cta = new Intent(mContext, ProxyService.class);
final PushNotificationProps notificationProps = new PushNotificationProps(bundle);
final PendingIntent pendingIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, cta, notificationProps);
NotificationCompat.Builder builder = CustomPushNotificationHelper.createNotificationBuilder(mContext, pendingIntent, bundle, false);
Notification notification = builder.build();
NotificationCompat.MessagingStyle messagingStyle = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(notification);
assert messagingStyle != null;
messagingStyle.addMessage(message, System.currentTimeMillis(), (Person)null);
notification = builder.setStyle(messagingStyle).build();
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(channel);
Notification notification =
new Notification.Builder(mContext, CHANNEL_ID)
.setContentTitle("Message failed to send.")
.setSmallIcon(smallIconResId)
.addExtras(userInfoBundle)
.build();
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
notificationManager.notify(notificationId, notification);
}
protected void onReplySuccess(NotificationManager notificationManager, int notificationId, String channelId) {
notificationManager.cancel(notificationId);
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
}
private CharSequence getReplyMessage(Intent intent) {
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
return remoteInput.getCharSequence(CustomPushNotificationHelper.KEY_TEXT_REPLY);
return remoteInput.getCharSequence(CustomPushNotification.KEY_TEXT_REPLY);
}
return null;
}

View File

@@ -0,0 +1,7 @@
package com.mattermost.rnbeta;
import android.net.Uri;
public interface RNEditTextOnPasteListener {
void onPaste(Uri itemUri);
}

View File

@@ -0,0 +1,97 @@
package com.mattermost.rnbeta;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
public class RNPasteableActionCallback implements ActionMode.Callback {
private RNPasteableEditText mEditText;
RNPasteableActionCallback(RNPasteableEditText editText) {
mEditText = editText;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Bundle config = MainApplication.instance.getManagedConfig();
if (config != null) {
WritableMap result = Arguments.fromBundle(config);
String copyPasteProtection = result.getString("copyAndPasteProtection");
if (copyPasteProtection.equals("true")) {
disableMenus(menu);
}
}
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Uri uri = this.getUriInClipboard();
if (item.getItemId() == android.R.id.paste && uri != null) {
mEditText.getOnPasteListener().onPaste(uri);
mode.finish();
} else {
mEditText.onTextContextMenuItem(item.getItemId());
}
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
}
private void disableMenus(Menu menu) {
for (int i = 0; i < menu.size(); i++) {
MenuItem item = menu.getItem(i);
int id = item.getItemId();
boolean shouldDisableMenu = (
id == android.R.id.paste
|| id == android.R.id.copy
|| id == android.R.id.cut
);
item.setEnabled(!shouldDisableMenu);
}
}
private Uri getUriInClipboard() {
ClipboardManager clipboardManager = (ClipboardManager) mEditText.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboardManager.getPrimaryClip();
if (clipData == null) {
return null;
}
ClipData.Item item = clipData.getItemAt(0);
if (item == null) {
return null;
}
CharSequence chars = item.getText();
if (chars == null) {
return null;
}
String text = chars.toString();
if (text.length() > 0) {
return null;
}
return item.getUri();
}
}

View File

@@ -0,0 +1,22 @@
package com.mattermost.rnbeta;
import android.content.Context;
import com.facebook.react.views.textinput.ReactEditText;
public class RNPasteableEditText extends ReactEditText {
private RNEditTextOnPasteListener mOnPasteListener;
public RNPasteableEditText(Context context) {
super(context);
}
public void setOnPasteListener(RNEditTextOnPasteListener listener) {
mOnPasteListener = listener;
}
public RNEditTextOnPasteListener getOnPasteListener() {
return mOnPasteListener;
}
}

View File

@@ -0,0 +1,156 @@
package com.mattermost.rnbeta;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.net.Uri;
import android.util.Patterns;
import android.webkit.MimeTypeMap;
import android.webkit.URLUtil;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.mattermost.share.RealPathUtil;
import com.mattermost.share.ShareModule;
import java.io.FileNotFoundException;
import java.io.File;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.regex.Matcher;
public class RNPasteableEditTextOnPasteListener implements RNEditTextOnPasteListener {
private RNPasteableEditText mEditText;
RNPasteableEditTextOnPasteListener(RNPasteableEditText editText) {
mEditText = editText;
}
@Override
public void onPaste(Uri itemUri) {
ReactContext reactContext = (ReactContext)mEditText.getContext();
String uri = itemUri.toString();
WritableArray images = null;
WritableMap error = null;
String uriMimeType = reactContext.getContentResolver().getType(itemUri);
if (uriMimeType == null) {
return;
}
// Special handle for Google docs
if (uri.equals("content://com.google.android.apps.docs.editors.kix.editors.clipboard")) {
ClipboardManager clipboardManager = (ClipboardManager) reactContext.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboardManager.getPrimaryClip();
if (clipData == null) {
return;
}
ClipData.Item item = clipData.getItemAt(0);
String htmlText = item.getHtmlText();
// Find uri from html
Matcher matcher = Patterns.WEB_URL.matcher(htmlText);
if (matcher.find()) {
uri = htmlText.substring(matcher.start(1), matcher.end());
}
}
if (uri.startsWith("http")) {
Thread pastImageFromUrlThread = new Thread(new RNPasteableImageFromUrl(reactContext, mEditText, uri));
pastImageFromUrlThread.start();
return;
}
uri = RealPathUtil.getRealPathFromURI(reactContext, itemUri);
if (uri == null) {
return;
}
// Get type
String extension = MimeTypeMap.getFileExtensionFromUrl(uri);
if (extension == null) {
return;
}
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mimeType == null) {
return;
}
// Get fileName
String fileName = URLUtil.guessFileName(uri, null, mimeType);
if (uri.contains(ShareModule.CACHE_DIR_NAME)) {
uri = moveToImagesCache(uri, fileName);
}
if (uri == null) {
return;
}
// Get fileSize
long fileSize;
try {
ContentResolver contentResolver = reactContext.getContentResolver();
AssetFileDescriptor assetFileDescriptor = contentResolver.openAssetFileDescriptor(itemUri, "r");
if (assetFileDescriptor == null) {
return;
}
fileSize = assetFileDescriptor.getLength();
WritableMap image = Arguments.createMap();
image.putString("type", mimeType);
image.putDouble("fileSize", fileSize);
image.putString("fileName", fileName);
image.putString("uri", "file://" + uri);
images = Arguments.createArray();
images.pushMap(image);
} catch (FileNotFoundException e) {
error = Arguments.createMap();
error.putString("message", e.getMessage());
}
WritableMap event = Arguments.createMap();
event.putArray("data", images);
event.putMap("error", error);
reactContext
.getJSModule(RCTEventEmitter.class)
.receiveEvent(
mEditText.getId(),
"onPaste",
event
);
}
private String moveToImagesCache(String src, String fileName) {
ReactContext ctx = (ReactContext)mEditText.getContext();
String cacheFolder = ctx.getCacheDir().getAbsolutePath() + "/Images/";
String dest = cacheFolder + fileName;
File folder = new File(cacheFolder);
try {
if (!folder.exists()) {
folder.mkdirs();
}
Files.move(Paths.get(src), Paths.get(dest));
} catch (FileAlreadyExistsException fileError) {
// Do nothing and return dest path
} catch (Exception err) {
return null;
}
return dest;
}
}

View File

@@ -0,0 +1,76 @@
package com.mattermost.rnbeta;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.facebook.react.views.textinput.ReactEditText;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
public class RNPasteableImageFromUrl implements Runnable {
private ReactContext mContext;
private String mUri;
private ReactEditText mTarget;
RNPasteableImageFromUrl(ReactContext context, ReactEditText target, String uri) {
mContext = context;
mUri = uri;
mTarget = target;
}
@Override
public void run() {
WritableArray images = null;
WritableMap error = null;
try {
URL url = new URL(mUri);
URLConnection u = url.openConnection();
// Get type
String mimeType = u.getHeaderField("Content-Type");
if (!mimeType.startsWith("image")) {
return;
}
// Get fileSize
long fileSize = Long.parseLong(u.getHeaderField("Content-Length"));
// Get fileName
String contentDisposition = u.getHeaderField("Content-Disposition");
int startIndex = contentDisposition.indexOf("filename=\"") + 10;
int endIndex = contentDisposition.length() - 1;
String fileName = contentDisposition.substring(startIndex, endIndex);
WritableMap image = Arguments.createMap();
image.putString("type", mimeType);
image.putDouble("fileSize", fileSize);
image.putString("fileName", fileName);
image.putString("uri", mUri);
images = Arguments.createArray();
images.pushMap(image);
} catch (IOException e) {
error = Arguments.createMap();
error.putString("message", e.getMessage());
}
WritableMap event = Arguments.createMap();
event.putArray("data", images);
event.putMap("error", error);
mContext
.getJSModule(RCTEventEmitter.class)
.receiveEvent(
mTarget.getId(),
"onPaste",
event
);
}
}

View File

@@ -0,0 +1,82 @@
package com.mattermost.rnbeta;
import androidx.core.view.inputmethod.EditorInfoCompat;
import androidx.core.view.inputmethod.InputConnectionCompat;
import androidx.core.os.BuildCompat;
import android.text.InputType;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.views.textinput.ReactEditText;
import com.facebook.react.views.textinput.ReactTextInputManager;
import java.util.Map;
import javax.annotation.Nullable;
public class RNPasteableTextInputManager extends ReactTextInputManager {
@Override
public String getName() {
return "PasteableTextInputAndroid";
}
@Override
public ReactEditText createViewInstance(ThemedReactContext context) {
RNPasteableEditText editText = new RNPasteableEditText(context) {
@Override
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
final InputConnection ic = super.onCreateInputConnection(editorInfo);
EditorInfoCompat.setContentMimeTypes(editorInfo,
new String [] {"image/*"});
final InputConnectionCompat.OnCommitContentListener callback =
(inputContentInfo, flags, opts) -> {
// read and display inputContentInfo asynchronously
if (BuildCompat.isAtLeastNMR1() && (flags &
InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
try {
inputContentInfo.requestPermission();
}
catch (Exception e) {
return false;
}
}
this.getOnPasteListener().onPaste(inputContentInfo.getContentUri());
return true;
};
return InputConnectionCompat.createWrapper(ic, editorInfo, callback);
}
};
int inputType = editText.getInputType();
editText.setInputType(inputType & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
editText.setReturnKeyType("done");
editText.setCustomInsertionActionModeCallback(new RNPasteableActionCallback(editText));
editText.setCustomSelectionActionModeCallback(new RNPasteableActionCallback(editText));
return editText;
}
@Override
protected void addEventEmitters(ThemedReactContext reactContext, ReactEditText editText) {
super.addEventEmitters(reactContext, editText);
RNPasteableEditText pasteableEditText = (RNPasteableEditText)editText;
pasteableEditText.setOnPasteListener(new RNPasteableEditTextOnPasteListener(pasteableEditText));
}
@Nullable
@Override
public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
Map<String, Object> map = super.getExportedCustomBubblingEventTypeConstants();
map.put(
"onPaste",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onPaste")));
return map;
}
}

View File

@@ -0,0 +1,28 @@
package com.mattermost.rnbeta;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
public class RNPasteableTextInputPackage implements ReactPackage {
@Nonnull
@Override
public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Nonnull
@Override
public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
return Arrays.asList(
new RNPasteableTextInputManager()
);
}
}

View File

@@ -4,19 +4,22 @@ import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.UIBlock;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import android.content.Context;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.NonNull;
public class RNTextInputResetModule extends ReactContextBaseJavaModule {
private final ReactApplicationContext reactContext;
public RNTextInputResetModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@Override
@NonNull
public String getName() {
return "RNTextInputReset";
}
@@ -25,13 +28,15 @@ public class RNTextInputResetModule extends ReactContextBaseJavaModule {
@ReactMethod
public void resetKeyboardInput(final int reactTagToReset) {
UIManagerModule uiManager = getReactApplicationContext().getNativeModule(UIManagerModule.class);
assert uiManager != null;
uiManager.addUIBlock(nativeViewHierarchyManager -> {
InputMethodManager imm = (InputMethodManager) getReactApplicationContext().getBaseContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
View viewToReset = nativeViewHierarchyManager.resolveView(reactTagToReset);
imm.restartInput(viewToReset);
uiManager.addUIBlock(new UIBlock() {
@Override
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
InputMethodManager imm = (InputMethodManager) getReactApplicationContext().getBaseContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
View viewToReset = nativeViewHierarchyManager.resolveView(reactTagToReset);
imm.restartInput(viewToReset);
}
}
});
}
}
}

View File

@@ -1,10 +1,12 @@
package com.mattermost.rnbeta;
import android.content.Context;
import androidx.annotation.Nullable;
import android.os.Bundle;
import android.util.Log;
import java.lang.System;
import okhttp3.Call;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
@@ -16,21 +18,43 @@ import org.json.JSONObject;
import org.json.JSONException;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Arguments;
import com.mattermost.react_native_interface.ResolvePromise;
public class ReceiptDelivery {
private static final int[] FIBONACCI_BACKOFF = new int[] { 0, 1, 2, 3, 5, 8 };
static final String CURRENT_SERVER_URL = "@currentServerUrl";
private static final int[] FIBONACCI_BACKOFFS = new int[] { 0, 1, 2, 3, 5, 8 };
public static void send(Context context, final String ackId, final String postId, final String type, final boolean isIdLoaded, ResolvePromise promise) {
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
final ReadableMap credentials = MattermostCredentialsHelper.getCredentialsSync(reactApplicationContext);
final String serverUrl = credentials.getString("serverUrl");
final String token = credentials.getString("token");
Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with ID-LOADED=%s", ackId, type, serverUrl, isIdLoaded));
execute(serverUrl, postId, token, ackId, type, isIdLoaded, promise);
MattermostCredentialsHelper.getCredentialsForCurrentServer(reactApplicationContext, new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
if (value instanceof Boolean && !(Boolean)value) {
return;
}
WritableMap map = (WritableMap) value;
if (map != null) {
String token = map.getString("password");
String serverUrl = map.getString("service");
if (serverUrl.isEmpty()) {
String[] credentials = token.split(",[ ]*");
if (credentials.length == 2) {
token = credentials[0];
serverUrl = credentials[1];
}
}
Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with ID-LOADED=%s", ackId, type, serverUrl, isIdLoaded));
execute(serverUrl, postId, token, ackId, type, isIdLoaded, promise);
}
}
});
}
protected static void execute(String serverUrl, String postId, String token, String ackId, String type, boolean isIdLoaded, ResolvePromise promise) {
@@ -60,7 +84,6 @@ public class ReceiptDelivery {
return;
}
assert serverUrl != null;
final HttpUrl url = HttpUrl.parse(
String.format("%s/api/v4/notifications/ack", serverUrl.replaceAll("/$", "")));
if (url != null) {
@@ -81,7 +104,6 @@ public class ReceiptDelivery {
private static void makeServerRequest(OkHttpClient client, Request request, Boolean isIdLoaded, int reRequestCount, ResolvePromise promise) {
try {
Response response = client.newCall(request).execute();
assert response.body() != null;
String responseBody = response.body().string();
if (response.code() != 200) {
switch (response.code()) {
@@ -107,8 +129,9 @@ public class ReceiptDelivery {
JSONObject jsonResponse = new JSONObject(responseBody);
Bundle bundle = new Bundle();
String[] keys = new String[]{"post_id", "root_id", "category", "message", "team_id", "channel_id", "channel_name", "type", "sender_id", "sender_name", "version"};
for (String key : keys) {
String keys[] = new String[]{"post_id", "category", "message", "team_id", "channel_id", "channel_name", "type", "sender_id", "sender_name", "version"};
for (int i = 0; i < keys.length; i++) {
String key = keys[i];
if (jsonResponse.has(key)) {
bundle.putString(key, jsonResponse.getString(key));
}
@@ -119,14 +142,12 @@ public class ReceiptDelivery {
if (isIdLoaded) {
try {
reRequestCount++;
if (reRequestCount < FIBONACCI_BACKOFF.length) {
Log.i("ReactNative", "Retry attempt " + reRequestCount + " with backoff delay: " + FIBONACCI_BACKOFF[reRequestCount] + " seconds");
Thread.sleep(FIBONACCI_BACKOFF[reRequestCount] * 1000);
makeServerRequest(client, request, true, reRequestCount, promise);
if (reRequestCount < FIBONACCI_BACKOFFS.length) {
Log.i("ReactNative", "Retry attempt " + reRequestCount + " with backoff delay: " + FIBONACCI_BACKOFFS[reRequestCount] + " seconds");
Thread.sleep(FIBONACCI_BACKOFFS[reRequestCount] * 1000);
makeServerRequest(client, request, isIdLoaded, reRequestCount, promise);
}
} catch(InterruptedException ie) {
// nothing to do
}
} catch(InterruptedException ie) {}
}
promise.reject("Receipt delivery failure", e.toString());

View File

@@ -3,9 +3,11 @@ package com.mattermost.share;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.content.ContentUris;
import android.content.ContentResolver;
import android.os.Environment;
import android.webkit.MimeTypeMap;
@@ -13,18 +15,18 @@ import android.util.Log;
import android.text.TextUtils;
import android.os.ParcelFileDescriptor;
import java.io.*;
import java.nio.channels.FileChannel;
import java.util.Objects;
// Class based on the steveevers DocumentHelper https://gist.github.com/steveevers/a5af24c226f44bb8fdc3
public class RealPathUtil {
public static String getRealPathFromURI(final Context context, final Uri uri) {
final boolean isKitKatOrNewer = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (DocumentsContract.isDocumentUri(context, uri)) {
if (isKitKatOrNewer && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
@@ -70,11 +72,7 @@ public class RealPathUtil {
split[1]
};
if (contentUri != null) {
return getDataColumn(context, contentUri, selection, selectionArgs);
} else {
return getPathFromSavingTempFile(context, uri);
}
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
@@ -98,26 +96,20 @@ public class RealPathUtil {
File tmpFile;
String fileName = null;
if (uri == null || uri.isRelative()) {
return 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 = sanitizeFilename(returnCursor.getString(nameIndex));
returnCursor.close();
fileName = returnCursor.getString(nameIndex);
} catch (Exception e) {
// just continue to get the filename with the last segment of the path
}
try {
if (TextUtils.isEmpty(fileName)) {
fileName = sanitizeFilename(uri.getLastPathSegment().trim());
if (fileName == null) {
fileName = uri.getLastPathSegment().toString().trim();
}
@@ -126,6 +118,7 @@ public class RealPathUtil {
cacheDir.mkdirs();
}
String mimeType = getMimeType(uri.getPath());
tmpFile = new File(cacheDir, fileName);
tmpFile.createNewFile();
@@ -232,18 +225,9 @@ public class RealPathUtil {
private static void deleteRecursive(File fileOrDirectory) {
if (fileOrDirectory.isDirectory())
for (File child : Objects.requireNonNull(fileOrDirectory.listFiles()))
for (File child : fileOrDirectory.listFiles())
deleteRecursive(child);
fileOrDirectory.delete();
}
private static String sanitizeFilename(String filename) {
if (filename == null) {
return null;
}
File f = new File(filename);
return f.getName();
}
}

View File

@@ -17,4 +17,9 @@ public class ShareActivity extends ReactActivity {
MainApplication app = (MainApplication) this.getApplication();
app.sharedExtensionIsOpened = true;
}
@Override
public void onBackPressed() {
finish();
}
}

View File

@@ -27,7 +27,6 @@ import org.json.JSONException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
@@ -46,7 +45,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
super(reactContext);
mApplication = application;
}
private File tempFolder;
@Override
@@ -133,7 +131,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
String text = "";
String type = "";
String action = "";
String extra = "";
Activity currentActivity = getCurrentActivity();
@@ -142,21 +139,20 @@ public class ShareModule extends ReactContextBaseJavaModule {
Intent intent = currentActivity.getIntent();
action = intent.getAction();
type = intent.getType();
extra = intent.getStringExtra(Intent.EXTRA_TEXT);
if (type == null) {
type = "";
}
if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type) && extra != null) {
map.putString("value", extra);
if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type)) {
text = intent.getStringExtra(Intent.EXTRA_TEXT);
map.putString("value", text);
map.putString("type", type);
map.putBoolean("isString", true);
items.pushMap(map);
} else if (Intent.ACTION_SEND.equals(action)) {
Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (uri != null) {
map.putString("value", "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri));
text = "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri);
map.putString("value", text);
if (type.equals("image/*")) {
type = "image/jpeg";
@@ -165,16 +161,17 @@ public class ShareModule extends ReactContextBaseJavaModule {
}
map.putString("type", type);
map.putBoolean("isString", false);
items.pushMap(map);
}
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
for (Uri uri : Objects.requireNonNull(uris)) {
for (Uri uri : uris) {
String filePath = RealPathUtil.getRealPathFromURI(currentActivity, uri);
map = Arguments.createMap();
map.putString("value", "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri));
type = RealPathUtil.getMimeTypeFromUri(currentActivity, uri);
text = "file://" + filePath;
map.putString("value", text);
type = RealPathUtil.getMimeTypeFromUri(currentActivity, uri);
if (type != null) {
if (type.equals("image/*")) {
type = "image/jpeg";
@@ -185,7 +182,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
type = "application/octet-stream";
}
map.putString("type", type);
map.putBoolean("isString", false);
items.pushMap(map);
}
}
@@ -225,7 +221,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);
for (int i = 0; i < files.size(); i++) {
for(int i = 0 ; i < files.size() ; i++) {
ReadableMap file = files.getMap(i);
String filePath = file.getString("fullPath").replaceFirst("file://", "");
File fileInfo = new File(filePath);
@@ -249,7 +245,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
JSONObject responseJson = new JSONObject(responseData);
JSONArray fileInfoArray = responseJson.getJSONArray("file_infos");
JSONArray file_ids = new JSONArray();
for (int i = 0; i < fileInfoArray.length(); i++) {
for(int i = 0 ; i < fileInfoArray.length() ; i++) {
JSONObject fileInfo = fileInfoArray.getJSONObject(i);
file_ids.put(fileInfo.getString("id"));
}

BIN
android/app/src/main/res/drawable-hdpi/splash.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 787 KiB

BIN
android/app/src/main/res/drawable-mdpi/splash.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

BIN
android/app/src/main/res/drawable-xhdpi/splash.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

BIN
android/app/src/main/res/drawable-xxhdpi/splash.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

BIN
android/app/src/main/res/drawable-xxxhdpi/splash.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -1,32 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/splashscreen_bg"
android:orientation="vertical">
android:background="#ffffff"
android:gravity="center_horizontal"
tools:context=".SplashScreenActivity">
<ImageView
android:id="@+id/splashBackgroundImage"
android:id="@+id/imgLogo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/splash_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/splashIcon"
android:layout_width="96dp"
android:layout_height="96dp"
android:contentDescription="@string/app_name"
android:src="@drawable/splash"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:adjustViewBounds="true"
android:src="@drawable/splash" />
</androidx.constraintlayout.widget.ConstraintLayout>

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 638 B

After

Width:  |  Height:  |  Size: 1.0 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 481 B

After

Width:  |  Height:  |  Size: 847 B

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 985 B

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