forked from Ivasoft/mattermost-mobile
Compare commits
24 Commits
main
...
release-2.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d653c4e19 | ||
|
|
fd25ea163d | ||
|
|
55578a0dce | ||
|
|
a9f325ef43 | ||
|
|
24ee8cc98e | ||
|
|
c27e1116cc | ||
|
|
2aaa366558 | ||
|
|
e72a142974 | ||
|
|
7165830fe0 | ||
|
|
ce5d049a55 | ||
|
|
8d9fab9b53 | ||
|
|
70cf8c5593 | ||
|
|
c9773d031d | ||
|
|
d75b854828 | ||
|
|
f1a06396c6 | ||
|
|
d1cbfe6659 | ||
|
|
ff18feeac4 | ||
|
|
05984b7202 | ||
|
|
511525c9ed | ||
|
|
055c9109ef | ||
|
|
d484a4ff45 | ||
|
|
e6a1cbb2aa | ||
|
|
c77f1dbd6d | ||
|
|
5f349e378e |
617
.circleci/config.yml
Normal file
617
.circleci/config.yml
Normal file
@@ -0,0 +1,617 @@
|
||||
version: 2.1
|
||||
orbs:
|
||||
owasp: entur/owasp@0.0.10
|
||||
node: circleci/node@5.0.3
|
||||
|
||||
executors:
|
||||
android:
|
||||
parameters:
|
||||
resource_class:
|
||||
default: xlarge
|
||||
type: string
|
||||
environment:
|
||||
NODE_OPTIONS: --max_old_space_size=12000
|
||||
NODE_ENV: production
|
||||
BABEL_ENV: production
|
||||
docker:
|
||||
- image: cimg/android:2022.09.2-node
|
||||
working_directory: ~/mattermost-mobile
|
||||
resource_class: <<parameters.resource_class>>
|
||||
|
||||
ios:
|
||||
parameters:
|
||||
resource_class:
|
||||
default: medium
|
||||
type: string
|
||||
environment:
|
||||
NODE_OPTIONS: --max_old_space_size=12000
|
||||
NODE_ENV: production
|
||||
BABEL_ENV: production
|
||||
macos:
|
||||
xcode: "14.0.0"
|
||||
working_directory: ~/mattermost-mobile
|
||||
shell: /bin/bash --login -o pipefail
|
||||
resource_class: <<parameters.resource_class>>
|
||||
|
||||
commands:
|
||||
checkout-private:
|
||||
description: "Checkout the private repo with build env vars"
|
||||
steps:
|
||||
- add_ssh_keys:
|
||||
fingerprints:
|
||||
- "03:1c:a7:07:35:bc:57:e4:1d:6c:e1:2c:4b:be:09:6d"
|
||||
- run:
|
||||
name: Clone the mobile private repo
|
||||
command: git clone git@github.com:mattermost/mattermost-mobile-private.git ~/mattermost-mobile-private
|
||||
|
||||
fastlane-dependencies:
|
||||
description: "Get Fastlane dependencies"
|
||||
parameters:
|
||||
for:
|
||||
type: string
|
||||
steps:
|
||||
- restore_cache:
|
||||
name: Restore Fastlane cache
|
||||
key: v1-gems-<< parameters.for >>-{{ checksum "fastlane/Gemfile.lock" }}-{{ arch }}
|
||||
- run:
|
||||
working_directory: fastlane
|
||||
name: Download Fastlane dependencies
|
||||
command: bundle install --path vendor/bundle
|
||||
- save_cache:
|
||||
name: Save Fastlane cache
|
||||
key: v1-gems-<< parameters.for >>-{{ checksum "fastlane/Gemfile.lock" }}-{{ arch }}
|
||||
paths:
|
||||
- fastlane/vendor/bundle
|
||||
|
||||
gradle-dependencies:
|
||||
description: "Get Gradle dependencies"
|
||||
steps:
|
||||
- restore_cache:
|
||||
name: Restore Gradle cache
|
||||
key: v1-gradle-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
|
||||
- run:
|
||||
working_directory: android
|
||||
name: Download Gradle dependencies
|
||||
command: ./gradlew dependencies
|
||||
- save_cache:
|
||||
name: Save Gradle cache
|
||||
paths:
|
||||
- ~/.gradle
|
||||
key: v1-gradle-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
|
||||
|
||||
assets:
|
||||
description: "Generate app assets"
|
||||
steps:
|
||||
- restore_cache:
|
||||
name: Restore assets cache
|
||||
key: v1-assets-{{ checksum "assets/base/config.json" }}-{{ arch }}
|
||||
- run:
|
||||
name: Generate assets
|
||||
command: node ./scripts/generate-assets.js
|
||||
- run:
|
||||
name: Compass Icons
|
||||
environment:
|
||||
COMPASS_ICONS: "node_modules/@mattermost/compass-icons/font/compass-icons.ttf"
|
||||
command: |
|
||||
cp "$COMPASS_ICONS" "assets/fonts/"
|
||||
cp "$COMPASS_ICONS" "android/app/src/main/assets/fonts"
|
||||
- save_cache:
|
||||
name: Save assets cache
|
||||
key: v1-assets-{{ checksum "assets/base/config.json" }}-{{ arch }}
|
||||
paths:
|
||||
- dist
|
||||
|
||||
npm-dependencies:
|
||||
description: "Get JavaScript dependencies"
|
||||
steps:
|
||||
- node/install:
|
||||
node-version: '18.7.0'
|
||||
- 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
|
||||
node node_modules/\@sentry/cli/scripts/install.js
|
||||
node node_modules/react-native-webrtc/tools/downloadWebRTC.js
|
||||
- save_cache:
|
||||
name: Save npm cache
|
||||
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
|
||||
paths:
|
||||
- node_modules
|
||||
- run:
|
||||
name: "Patch dependencies"
|
||||
command: npx patch-package
|
||||
|
||||
pods-dependencies:
|
||||
description: "Get cocoapods dependencies"
|
||||
steps:
|
||||
- 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
|
||||
- save_cache:
|
||||
name: Save cocoapods specs and pods cache
|
||||
key: v1-cocoapods-{{ checksum "ios/Podfile.lock" }}-{{ arch }}
|
||||
paths:
|
||||
- ios/Pods
|
||||
- ~/.cocoapods
|
||||
|
||||
build-android:
|
||||
description: "Build the android app"
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- checkout-private
|
||||
- npm-dependencies
|
||||
- assets
|
||||
- fastlane-dependencies:
|
||||
for: android
|
||||
- gradle-dependencies
|
||||
- run:
|
||||
name: Append Keystore to build Android
|
||||
command: |
|
||||
cp ~/mattermost-mobile-private/android/${STORE_FILE} android/app/${STORE_FILE}
|
||||
echo "" | tee -a android/gradle.properties > /dev/null
|
||||
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
|
||||
|
||||
build-ios:
|
||||
description: "Build the iOS app"
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- npm-dependencies
|
||||
- pods-dependencies
|
||||
- assets
|
||||
- fastlane-dependencies:
|
||||
for: ios
|
||||
- run:
|
||||
working_directory: fastlane
|
||||
name: Run fastlane to build iOS
|
||||
no_output_timeout: 30m
|
||||
command: |
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
|
||||
export TERM=xterm && bundle exec fastlane ios build
|
||||
|
||||
deploy-to-store:
|
||||
description: "Deploy build to store"
|
||||
parameters:
|
||||
task:
|
||||
type: string
|
||||
target:
|
||||
type: string
|
||||
file:
|
||||
type: string
|
||||
env:
|
||||
type: string
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- run:
|
||||
name: <<parameters.task>>
|
||||
working_directory: fastlane
|
||||
command: <<parameters.env>> bundle exec fastlane <<parameters.target>> deploy file:$HOME/mattermost-mobile/<<parameters.file>>
|
||||
|
||||
persist:
|
||||
description: "Persist mattermost-mobile directory"
|
||||
steps:
|
||||
- persist_to_workspace:
|
||||
root: ~/
|
||||
paths:
|
||||
- mattermost-mobile*
|
||||
|
||||
save:
|
||||
description: "Save binaries artifacts"
|
||||
parameters:
|
||||
filename:
|
||||
type: string
|
||||
steps:
|
||||
- run:
|
||||
name: Copying artifacts
|
||||
command: |
|
||||
mkdir /tmp/artifacts;
|
||||
cp ~/mattermost-mobile/<<parameters.filename>> /tmp/artifacts;
|
||||
- store_artifacts:
|
||||
path: /tmp/artifacts
|
||||
|
||||
jobs:
|
||||
test:
|
||||
working_directory: ~/mattermost-mobile
|
||||
docker:
|
||||
- image: cimg/node:16.14.2
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- npm-dependencies
|
||||
- assets
|
||||
- run:
|
||||
name: Check styles
|
||||
command: npm run check
|
||||
- run:
|
||||
name: Running Tests
|
||||
command: npm test
|
||||
- run:
|
||||
name: Check i18n
|
||||
command: ./scripts/precommit/i18n.sh
|
||||
|
||||
check-deps:
|
||||
parameters:
|
||||
cve_data_directory:
|
||||
type: string
|
||||
default: "~/.owasp/dependency-check-data"
|
||||
working_directory: ~/mattermost-mobile
|
||||
executor: owasp/default
|
||||
environment:
|
||||
version_url: "https://jeremylong.github.io/DependencyCheck/current.txt"
|
||||
executable_url: "https://dl.bintray.com/jeremy-long/owasp/dependency-check-VERSION-release.zip"
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
name: Restore npm cache
|
||||
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
|
||||
- run:
|
||||
name: Checkout config
|
||||
command: cd .. && git clone https://github.com/mattermost/security-automation-config
|
||||
- run:
|
||||
name: Install Go
|
||||
command: sudo apt-get update && sudo apt-get install golang
|
||||
- owasp/with_commandline:
|
||||
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
|
||||
- owasp/restore_owasp_cache
|
||||
- run:
|
||||
name: Update OWASP Dependency-Check Database
|
||||
command: |
|
||||
if ! ~/.owasp/dependency-check/bin/dependency-check.sh --data << parameters.cve_data_directory >> --updateonly; then
|
||||
# Update failed, probably due to a bad DB version; delete cached DB and try again
|
||||
rm -rv ~/.owasp/dependency-check-data/*.db
|
||||
~/.owasp/dependency-check/bin/dependency-check.sh --data << parameters.cve_data_directory >> --updateonly
|
||||
fi
|
||||
- owasp/store_owasp_cache:
|
||||
cve_data_directory: <<parameters.cve_data_directory>>
|
||||
- run:
|
||||
name: Run OWASP Dependency-Check Analyzer
|
||||
command: |
|
||||
~/.owasp/dependency-check/bin/dependency-check.sh \
|
||||
--data << parameters.cve_data_directory >> --format ALL --noupdate --enableExperimental \
|
||||
--propertyfile ../security-automation-config/dependency-check/dependencycheck.properties \
|
||||
--suppression ../security-automation-config/dependency-check/suppression.xml \
|
||||
--suppression ../security-automation-config/dependency-check/suppression.$CIRCLE_PROJECT_REPONAME.xml \
|
||||
--scan './**/*' || true
|
||||
- owasp/collect_reports:
|
||||
persist_to_workspace: false
|
||||
- run:
|
||||
name: Post results to Mattermost
|
||||
command: go run ../security-automation-config/dependency-check/post_results.go
|
||||
|
||||
build-android-beta:
|
||||
executor: android
|
||||
steps:
|
||||
- build-android
|
||||
- persist
|
||||
- save:
|
||||
filename: "*.apk"
|
||||
|
||||
build-android-release:
|
||||
executor: android
|
||||
steps:
|
||||
- build-android
|
||||
- persist
|
||||
- save:
|
||||
filename: "*.apk"
|
||||
|
||||
build-android-pr:
|
||||
executor: android
|
||||
environment:
|
||||
BRANCH_TO_BUILD: ${CIRCLE_BRANCH}
|
||||
steps:
|
||||
- build-android
|
||||
- save:
|
||||
filename: "*.apk"
|
||||
|
||||
build-android-unsigned:
|
||||
executor: android
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- npm-dependencies
|
||||
- assets
|
||||
- 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
|
||||
no_output_timeout: 30m
|
||||
command: bundle exec fastlane android unsigned
|
||||
- persist
|
||||
- save:
|
||||
filename: "*.apk"
|
||||
|
||||
build-ios-beta:
|
||||
executor:
|
||||
name: ios
|
||||
resource_class: large
|
||||
steps:
|
||||
- build-ios
|
||||
- persist
|
||||
- save:
|
||||
filename: "*.ipa"
|
||||
|
||||
build-ios-release:
|
||||
executor: ios
|
||||
steps:
|
||||
- build-ios
|
||||
- persist
|
||||
- save:
|
||||
filename: "*.ipa"
|
||||
|
||||
build-ios-pr:
|
||||
executor: ios
|
||||
environment:
|
||||
BRANCH_TO_BUILD: ${CIRCLE_BRANCH}
|
||||
steps:
|
||||
- build-ios
|
||||
- save:
|
||||
filename: "*.ipa"
|
||||
|
||||
build-ios-unsigned:
|
||||
executor: ios
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- npm-dependencies
|
||||
- pods-dependencies
|
||||
- assets
|
||||
- fastlane-dependencies:
|
||||
for: ios
|
||||
- run:
|
||||
working_directory: fastlane
|
||||
name: Run fastlane to build unsigned iOS
|
||||
no_output_timeout: 30m
|
||||
command: |
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
|
||||
bundle exec fastlane ios unsigned
|
||||
- persist_to_workspace:
|
||||
root: ~/
|
||||
paths:
|
||||
- mattermost-mobile/*.ipa
|
||||
- save:
|
||||
filename: "*.ipa"
|
||||
|
||||
build-ios-simulator:
|
||||
executor: ios
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- npm-dependencies
|
||||
- pods-dependencies
|
||||
- assets
|
||||
- fastlane-dependencies:
|
||||
for: ios
|
||||
- run:
|
||||
working_directory: fastlane
|
||||
name: Run fastlane to build unsigned x86_64 iOS app for iPhone simulator
|
||||
no_output_timeout: 30m
|
||||
command: |
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
|
||||
bundle exec fastlane ios simulator
|
||||
- persist_to_workspace:
|
||||
root: ~/
|
||||
paths:
|
||||
- mattermost-mobile/Mattermost-simulator-x86_64.app.zip
|
||||
- save:
|
||||
filename: "Mattermost-simulator-x86_64.app.zip"
|
||||
|
||||
deploy-android-release:
|
||||
executor:
|
||||
name: android
|
||||
resource_class: medium
|
||||
steps:
|
||||
- deploy-to-store:
|
||||
task: "Deploy to Google Play"
|
||||
target: android
|
||||
file: "*.apk"
|
||||
env: "SUPPLY_TRACK=beta"
|
||||
|
||||
deploy-android-beta:
|
||||
executor:
|
||||
name: android
|
||||
resource_class: medium
|
||||
steps:
|
||||
- deploy-to-store:
|
||||
task: "Deploy to Google Play"
|
||||
target: android
|
||||
file: "*.apk"
|
||||
env: "SUPPLY_TRACK=alpha"
|
||||
|
||||
deploy-ios-release:
|
||||
executor: ios
|
||||
steps:
|
||||
- deploy-to-store:
|
||||
task: "Deploy to TestFlight"
|
||||
target: ios
|
||||
file: "*.ipa"
|
||||
env: ""
|
||||
|
||||
deploy-ios-beta:
|
||||
executor: ios
|
||||
steps:
|
||||
- deploy-to-store:
|
||||
task: "Deploy to TestFlight"
|
||||
target: ios
|
||||
file: "*.ipa"
|
||||
env: ""
|
||||
|
||||
github-release:
|
||||
executor:
|
||||
name: android
|
||||
resource_class: medium
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- run:
|
||||
name: Create GitHub release
|
||||
working_directory: fastlane
|
||||
command: bundle exec fastlane github
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- test
|
||||
# - check-deps:
|
||||
# context: sast-webhook
|
||||
# requires:
|
||||
# - test
|
||||
|
||||
- build-android-release:
|
||||
context: mattermost-mobile-android-release
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-release-\d+$/
|
||||
- /^build-android-release-\d+$/
|
||||
- deploy-android-release:
|
||||
context: mattermost-mobile-android-release
|
||||
requires:
|
||||
- build-android-release
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-release-\d+$/
|
||||
- /^build-android-release-\d+$/
|
||||
|
||||
- build-android-beta:
|
||||
context: mattermost-mobile-android-beta
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-\d+$/
|
||||
- /^build-android-\d+$/
|
||||
- /^build-android-beta-\d+$/
|
||||
- deploy-android-beta:
|
||||
context: mattermost-mobile-android-beta
|
||||
requires:
|
||||
- build-android-beta
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-\d+$/
|
||||
- /^build-android-\d+$/
|
||||
- /^build-android-beta-\d+$/
|
||||
|
||||
- build-ios-release:
|
||||
context: mattermost-mobile-ios-release
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-release-\d+$/
|
||||
- /^build-ios-release-\d+$/
|
||||
- deploy-ios-release:
|
||||
context: mattermost-mobile-ios-release
|
||||
requires:
|
||||
- build-ios-release
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-release-\d+$/
|
||||
- /^build-ios-release-\d+$/
|
||||
|
||||
- build-ios-beta:
|
||||
context: mattermost-mobile-ios-beta
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-\d+$/
|
||||
- /^build-ios-\d+$/
|
||||
- /^build-ios-beta-\d+$/
|
||||
- deploy-ios-beta:
|
||||
context: mattermost-mobile-ios-beta
|
||||
requires:
|
||||
- build-ios-beta
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-\d+$/
|
||||
- /^build-ios-\d+$/
|
||||
- /^build-ios-beta-\d+$/
|
||||
|
||||
- build-android-pr:
|
||||
context: mattermost-mobile-android-pr
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only: /^(build|android)-pr-.*/
|
||||
- build-ios-pr:
|
||||
context: mattermost-mobile-ios-pr
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only: /^(build|ios)-pr-.*/
|
||||
|
||||
- build-android-unsigned:
|
||||
context: mattermost-mobile-unsigned
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
tags:
|
||||
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
branches:
|
||||
only: unsigned
|
||||
- build-ios-unsigned:
|
||||
context: mattermost-mobile-unsigned
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
tags:
|
||||
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
branches:
|
||||
only: unsigned
|
||||
- build-ios-simulator:
|
||||
context: mattermost-mobile-unsigned
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-\d+$/
|
||||
- /^build-ios-sim-\d+$/
|
||||
|
||||
- github-release:
|
||||
context: mattermost-mobile-unsigned
|
||||
requires:
|
||||
- build-android-unsigned
|
||||
- build-ios-unsigned
|
||||
filters:
|
||||
tags:
|
||||
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
branches:
|
||||
only: unsigned
|
||||
94
.drone.yml
94
.drone.yml
@@ -1,94 +0,0 @@
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: private assets
|
||||
image: alpine/git
|
||||
settings:
|
||||
username:
|
||||
from_secret: repo_user
|
||||
password:
|
||||
from_secret: repo_pass
|
||||
commands:
|
||||
- chmod -R 777 .
|
||||
- git clone https://$$PLUGIN_USERNAME:$$PLUGIN_PASSWORD@git.ivasoft.cz/Ivasoft/mattermost-mobile-private.git ./mattermost-mobile-private
|
||||
|
||||
- name: build-android-release_exprojekt
|
||||
image: git.ivasoft.cz/sw/github-actions
|
||||
settings:
|
||||
#action_image: catthehacker/ubuntu:act-22.04 # based on ubuntu:22.04 as required by ruby installer
|
||||
action_image: mingc/android-build-box:1.25.0
|
||||
daemon_off: true
|
||||
uses: ./.drone/build-android-release
|
||||
with:
|
||||
company: exprojekt
|
||||
environment:
|
||||
STORE_PASSWORD_INPUT:
|
||||
from_secret: android_keystore_pass
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: minio_build_access_key
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: minio_build_secret_key
|
||||
when:
|
||||
target:
|
||||
- production
|
||||
- android
|
||||
|
||||
- name: build-ios-release_exprojekt
|
||||
image: git.ivasoft.cz/sw/github-actions
|
||||
settings:
|
||||
action_image: osx:act
|
||||
no_force_pull: true
|
||||
daemon_off: true
|
||||
kvm: true
|
||||
#verbose: true
|
||||
uses: ./.drone/build-ios-release
|
||||
with:
|
||||
company: exprojekt
|
||||
teamId: SX6ZHT7MDU
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: minio_build_access_key
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: minio_build_secret_key
|
||||
APPSTORE_APP_PASSWORD:
|
||||
from_secret: appstore_app_password
|
||||
when:
|
||||
target:
|
||||
- production
|
||||
- ios
|
||||
|
||||
- name: build-ios-beta_exprojekt
|
||||
image: git.ivasoft.cz/sw/github-actions
|
||||
settings:
|
||||
action_image: osx:act
|
||||
no_force_pull: true
|
||||
daemon_off: true
|
||||
kvm: true
|
||||
#verbose: true
|
||||
uses: ./.drone/build-ios-beta
|
||||
with:
|
||||
company: exprojekt
|
||||
teamId: SX6ZHT7MDU
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID:
|
||||
from_secret: minio_build_access_key
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
from_secret: minio_build_secret_key
|
||||
APPSTORE_APP_PASSWORD:
|
||||
from_secret: appstore_app_password
|
||||
when:
|
||||
target:
|
||||
- beta
|
||||
|
||||
- name: gitea_release
|
||||
image: plugins/gitea-release
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: drone_release
|
||||
base_url: https://git.ivasoft.cz
|
||||
files:
|
||||
- 'Mattermost.apk'
|
||||
- '*.ipa'
|
||||
when:
|
||||
event: tag
|
||||
@@ -1,53 +0,0 @@
|
||||
name: droid-build-android-release
|
||||
description: Equivalent to build-android-release workflow on GitHub
|
||||
|
||||
inputs:
|
||||
company:
|
||||
description: Company whose release to release
|
||||
default: "exprojekt"
|
||||
title:
|
||||
description: Company whose release to release
|
||||
default: "EXprojekt Team"
|
||||
#store-password:
|
||||
# description: Password for the keystore
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: ci/prepare-android-company-overrides
|
||||
env:
|
||||
COMPANY_SOURCE_PATH: "mattermost-mobile-private/android/${{ inputs.company }}"
|
||||
run: |
|
||||
mkdir -p assets/override
|
||||
cp -R -f ${COMPANY_SOURCE_PATH}/assets/* assets/override/
|
||||
cp -f ${COMPANY_SOURCE_PATH}/android/app/google-services.json android/app/
|
||||
|
||||
- name: ci/prepare-android-build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
env:
|
||||
STORE_FILE: "${{ inputs.company }}.keystore"
|
||||
STORE_ALIAS: "MATTERMOST_RELEASE_KEY_ALIAS=mattermost-google-key"
|
||||
STORE_PASSWORD: "MATTERMOST_RELEASE_PASSWORD=${{ env.STORE_PASSWORD_INPUT }}"
|
||||
|
||||
- name: ci/prepare-android-build-fix
|
||||
run: |
|
||||
sed -i -E "s:^(SENTRY_ENABLED\=)(.*)$:\1false:" fastlane/.env.android.release
|
||||
sed -i -E "s:^(APP_NAME\=)(.*)$:\1${{ inputs.title }}:" fastlane/.env.android.release
|
||||
sed -i -E "s:^(APP_SCHEME\=)(.*)$:\1mm\-${{ inputs.company }}:" fastlane/.env.android.release
|
||||
sed -i -E "s:^(MAIN_APP_IDENTIFIER\=)(.*)$:\1cz.${{ inputs.company }}.team:" fastlane/.env.android.release
|
||||
sed -i -E "s:^(SUPPLY_PACKAGE_NAME\=)(.*)$:\1cz.${{ inputs.company }}.team:" fastlane/.env.android.release
|
||||
sed -i -E "s:^(org\.gradle\.jvmargs\=)(.*)(\-Xmx)([[:digit:]]+m)(.*)(\-XX\:MaxMetaspaceSize\=)([[:digit:]]+m)(.*)$:\1\2\34096m\5\61024m\8:" android/gradle.properties
|
||||
sed -i -E "s:^([[:space:]]*)(.*)(Aws\:\:S3\:\:Resource\.new)(.*)$:\1Aws.config.update\(endpoint\:'http\://storage.cluster\:81',force_path_style\:true\)\n\1\2\3\4:" fastlane/Fastfile
|
||||
|
||||
- name: ci/build-and-deploy-android-release
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: "${{ env.AWS_ACCESS_KEY_ID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ env.AWS_SECRET_ACCESS_KEY }}"
|
||||
run: |
|
||||
echo "::group::Build"
|
||||
bundle exec fastlane android build --env android.release
|
||||
echo "::endgroup::"
|
||||
echo "::group::Deploy to Play Store"
|
||||
bundle exec fastlane android deploy file:"${{ github.workspace }}/*.apk" --env android.release
|
||||
echo "::endgroup::"
|
||||
working-directory: ./fastlane
|
||||
@@ -1,91 +0,0 @@
|
||||
name: droid-build-ios-beta
|
||||
description: Equivalent to build-ios-beta workflow on GitHub
|
||||
|
||||
inputs:
|
||||
company:
|
||||
description: Company whose beta to release
|
||||
default: "exprojekt"
|
||||
title:
|
||||
description: App title whose beta to release
|
||||
default: "EXprojekt Team"
|
||||
teamId:
|
||||
description: App Store Team ID
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: ci/create-keychain-beta
|
||||
uses: ./.drone/create-keychain
|
||||
with:
|
||||
path: /tmp/beta-${{ inputs.company }}-keychain
|
||||
- name: ci/add-certs
|
||||
env:
|
||||
KEYCHAIN_PATH: /tmp/beta-${{ inputs.company }}-keychain
|
||||
shell: bash
|
||||
run: |
|
||||
security import ${{ github.workspace }}/mattermost-mobile-private/ios/AppleWWDRCAG3.cer -k $KEYCHAIN_PATH
|
||||
security import ${{ github.workspace }}/mattermost-mobile-private/ios/AppleWWDRCAG4.cer -k $KEYCHAIN_PATH
|
||||
security import ${{ github.workspace }}/mattermost-mobile-private/ios/${{ inputs.company }}/dev.cer -k $KEYCHAIN_PATH
|
||||
security unlock-keychain -p a $KEYCHAIN_PATH
|
||||
security import ${{ github.workspace }}/mattermost-mobile-private/ios/${{ inputs.company }}/dev.key -k $KEYCHAIN_PATH -A -T /usr/bin/codesign -T /usr/bin/security
|
||||
security import ${{ github.workspace }}/mattermost-mobile-private/ios/${{ inputs.company }}/notify.cer -k $KEYCHAIN_PATH
|
||||
security import ${{ github.workspace }}/mattermost-mobile-private/ios/${{ inputs.company }}/notify.key -k $KEYCHAIN_PATH -A -T /usr/bin/codesign -T /usr/bin/security
|
||||
- name: ci/complete-keychain-beta
|
||||
uses: ./.drone/complete-keychain
|
||||
with:
|
||||
path: /tmp/beta-${{ inputs.company }}-keychain
|
||||
|
||||
- name: ci/profile-app-beta
|
||||
uses: ./.drone/install-ios-profile
|
||||
with:
|
||||
company: ${{ inputs.company }}
|
||||
name: TeamCechTest.mobileprovision
|
||||
- name: ci/profile-app-notify-beta
|
||||
uses: ./.drone/install-ios-profile
|
||||
with:
|
||||
company: ${{ inputs.company }}
|
||||
name: TeamNotifyCechTest.mobileprovision
|
||||
- name: ci/profile-app-share-beta
|
||||
uses: ./.drone/install-ios-profile
|
||||
with:
|
||||
company: ${{ inputs.company }}
|
||||
name: TeamShareCechTest.mobileprovision
|
||||
|
||||
- name: ci/prepare-ios-beta-fix
|
||||
uses: ./.drone/fix-ios-build
|
||||
with:
|
||||
company: ${{ inputs.company }}
|
||||
title: ${{ inputs.title }}
|
||||
envPath: fastlane/.env.ios.beta
|
||||
provisioningProfileTeam: TeamCechTest
|
||||
provisioningProfileNotify: TeamNotifyCechTest
|
||||
provisioningProfileShare: TeamShareCechTest
|
||||
|
||||
- name: ci/prepare-ios-build-beta
|
||||
env:
|
||||
LC_ALL: en_US.UTF-8
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
||||
- name: ci/build-and-deploy-ios-beta
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: "${{ env.AWS_ACCESS_KEY_ID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ env.AWS_SECRET_ACCESS_KEY }}"
|
||||
APPSTORE_APP_PASSWORD: "${{ env.APPSTORE_APP_PASSWORD }}"
|
||||
FASTLANE_TEAM_ID: "${{ inputs.teamId }}"
|
||||
# Assign provisioning profiles in Xcode project
|
||||
sigh_cz.exprojekt.team_appstore_profile-path: mattermost-mobile-private/ios/${{ inputs.company }}/TeamCechTest.mobileprovision
|
||||
sigh_cz.exprojekt.team.NotificationService_appstore_profile-path: mattermost-mobile-private/ios/${{ inputs.company }}/TeamNotifyCechTest.mobileprovision
|
||||
sigh_cz.exprojekt.team.MattermostShare_appstore_profile-path: mattermost-mobile-private/ios/${{ inputs.company }}/TeamShareCechTest.mobileprovision
|
||||
LC_ALL: en_US.UTF-8
|
||||
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: "120"
|
||||
KEYCHAIN_PATH: /tmp/beta-${{ inputs.company }}-keychain
|
||||
run: |
|
||||
echo "Unlocking keychain"
|
||||
security unlock-keychain -p a $KEYCHAIN_PATH
|
||||
echo "::group::Build"
|
||||
bundle exec fastlane ios build --env ios.beta
|
||||
echo "::endgroup::"
|
||||
echo "::group::Deploy to TestFlight"
|
||||
echo xcrun altool --upload-app -f "${{ github.workspace }}/EXprojekt_Team.ipa" -t ios -u apple@exprojekt.cz -p "$APPSTORE_APP_PASSWORD"
|
||||
echo "::endgroup::"
|
||||
working-directory: ./fastlane
|
||||
@@ -1,115 +0,0 @@
|
||||
name: droid-build-ios-release
|
||||
description: Equivalent to build-ios-release workflow on GitHub
|
||||
|
||||
inputs:
|
||||
company:
|
||||
description: Company whose release to release
|
||||
default: "exprojekt"
|
||||
title:
|
||||
description: App title whose release to release
|
||||
default: "EXprojekt Team"
|
||||
teamId:
|
||||
description: App Store Team ID
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: ci/prepare-ios-company-overrides
|
||||
env:
|
||||
COMPANY_SOURCE_PATH: "mattermost-mobile-private/ios/${{ inputs.company }}"
|
||||
run: |
|
||||
mkdir -p assets/override
|
||||
cp -R -f ${COMPANY_SOURCE_PATH}/assets/* assets/override/
|
||||
|
||||
- name: ci/create-keychain-release
|
||||
uses: ./.drone/create-keychain
|
||||
with:
|
||||
path: /tmp/release-${{ inputs.company }}-keychain
|
||||
- name: ci/add-certs
|
||||
env:
|
||||
KEYCHAIN_PATH: /tmp/release-${{ inputs.company }}-keychain
|
||||
shell: bash
|
||||
run: |
|
||||
security import ${{ github.workspace }}/mattermost-mobile-private/ios/AppleWWDRCAG3.cer -k $KEYCHAIN_PATH
|
||||
security import ${{ github.workspace }}/mattermost-mobile-private/ios/AppleWWDRCAG4.cer -k $KEYCHAIN_PATH
|
||||
security import ${{ github.workspace }}/mattermost-mobile-private/ios/${{ inputs.company }}/distr.cer -k $KEYCHAIN_PATH
|
||||
security unlock-keychain -p a $KEYCHAIN_PATH
|
||||
security import ${{ github.workspace }}/mattermost-mobile-private/ios/${{ inputs.company }}/distr.key -k $KEYCHAIN_PATH -A -T /usr/bin/codesign -T /usr/bin/security
|
||||
security import ${{ github.workspace }}/mattermost-mobile-private/ios/${{ inputs.company }}/notify.cer -k $KEYCHAIN_PATH
|
||||
security import ${{ github.workspace }}/mattermost-mobile-private/ios/${{ inputs.company }}/notify.key -k $KEYCHAIN_PATH -A -T /usr/bin/codesign -T /usr/bin/security
|
||||
- name: ci/complete-keychain-release
|
||||
uses: ./.drone/complete-keychain
|
||||
with:
|
||||
path: /tmp/release-${{ inputs.company }}-keychain
|
||||
|
||||
- name: ci/profile-app-release
|
||||
uses: ./.drone/install-ios-profile
|
||||
with:
|
||||
company: ${{ inputs.company }}
|
||||
name: TeamAppStore.mobileprovision
|
||||
- name: ci/profile-app-notify-release
|
||||
uses: ./.drone/install-ios-profile
|
||||
with:
|
||||
company: ${{ inputs.company }}
|
||||
name: TeamNotifyAppStore.mobileprovision
|
||||
- name: ci/profile-app-share-release
|
||||
uses: ./.drone/install-ios-profile
|
||||
with:
|
||||
company: ${{ inputs.company }}
|
||||
name: TeamShareAppStore.mobileprovision
|
||||
|
||||
- name: ci/prepare-ios-release-fix
|
||||
uses: ./.drone/fix-ios-build
|
||||
with:
|
||||
company: ${{ inputs.company }}
|
||||
title: ${{ inputs.title }}
|
||||
envPath: fastlane/.env.ios.release
|
||||
provisioningProfileTeam: TeamAppStore
|
||||
provisioningProfileNotify: TeamNotifyAppStore
|
||||
provisioningProfileShare: TeamShareAppStore
|
||||
|
||||
#- name: ci/set-build-number-ios-release
|
||||
# run: |
|
||||
# echo -e "\nBUILD_NUMBER=519\nINCREMENT_BUILD_NUMBER=true" >> fastlane/.env.ios.release
|
||||
# HACK: build numbers are broken change it manually after build
|
||||
# 1. extract .ipa (ie. using peazip)
|
||||
# 2. codesign -d --entitlements Entitlements.plist EXprojekt_Team/Payload/Mattermost.app
|
||||
# 3. plutil -replace CFBundleVersion -string 519 EXprojekt_Team/Payload/Mattermost.app/Info.plist
|
||||
# 4. delete EXprojekt_Team/Payload/Mattermost.app/_CodeSignature
|
||||
# 5. security import mattermost-mobile-private/ios/exprojekt/distr.pem
|
||||
# 6. security import mattermost-mobile-private/ios/exprojekt/distr.key
|
||||
# 7. security find-identity (and determine id of the Apple Distribution certificate)
|
||||
# 8. codesign -s <id from previous step> -f --entitlements Entitlements.plist EXprojekt_Team/Payload/Mattermost.app
|
||||
# 9. re-zip the directory to .ipa
|
||||
|
||||
- name: ci/prepare-ios-build-release
|
||||
env:
|
||||
LC_ALL: en_US.UTF-8
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
||||
- name: ci/build-and-deploy-ios-release
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: "${{ env.AWS_ACCESS_KEY_ID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ env.AWS_SECRET_ACCESS_KEY }}"
|
||||
APPSTORE_APP_PASSWORD: "${{ env.APPSTORE_APP_PASSWORD }}"
|
||||
FASTLANE_TEAM_ID: "${{ inputs.teamId }}"
|
||||
# Assign provisioning profiles in Xcode project
|
||||
sigh_cz.exprojekt.team_appstore_profile-path: mattermost-mobile-private/ios/${{ inputs.company }}/TeamAppStore.mobileprovision
|
||||
sigh_cz.exprojekt.team.NotificationService_appstore_profile-path: mattermost-mobile-private/ios/${{ inputs.company }}/TeamNotifyAppStore.mobileprovision
|
||||
sigh_cz.exprojekt.team.MattermostShare_appstore_profile-path: mattermost-mobile-private/ios/${{ inputs.company }}/TeamShareAppStore.mobileprovision
|
||||
LC_ALL: en_US.UTF-8
|
||||
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: "120"
|
||||
KEYCHAIN_PATH: /tmp/release-${{ inputs.company }}-keychain
|
||||
run: |
|
||||
echo "Unlocking keychain"
|
||||
security unlock-keychain -p a $KEYCHAIN_PATH
|
||||
echo "Changing build number"
|
||||
echo -e "\nBUILD_NUMBER=666\nINCREMENT_BUILD_NUMBER=true" >> .env.ios.release
|
||||
bundle exec fastlane set_app_build_number --env ios.release
|
||||
echo "::group::Build"
|
||||
bundle exec fastlane ios build --env ios.release
|
||||
echo "::endgroup::"
|
||||
echo "::group::Deploy to TestFlight"
|
||||
echo xcrun altool --upload-app -f "${{ github.workspace }}/EXprojekt_Team.ipa" -t ios -u apple@exprojekt.cz -p "$APPSTORE_APP_PASSWORD"
|
||||
echo "::endgroup::"
|
||||
working-directory: ./fastlane
|
||||
@@ -1,16 +0,0 @@
|
||||
name: droid-complete-keychain
|
||||
description: Completes preparation of a keychain for signing certificates
|
||||
|
||||
inputs:
|
||||
path:
|
||||
description: Path to keychain to complete
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: ci/complete-keychain
|
||||
shell: bash
|
||||
run: |
|
||||
security set-key-partition-list -S apple-tool:,apple: -k a ${{ inputs.path }}
|
||||
security list-keychain -d user -s ${{ inputs.path }}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
name: droid-create-keychain
|
||||
description: Prepares a keychain for signing certificates
|
||||
|
||||
inputs:
|
||||
path:
|
||||
description: Path to keychain to create
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: ci/create-keychain
|
||||
shell: bash
|
||||
run: |
|
||||
security create-keychain -p a ${{ inputs.path }}
|
||||
security set-keychain-settings -lut 21600 ${{ inputs.path }}
|
||||
security unlock-keychain -p a ${{ inputs.path }}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
name: droid-fix-ios-build
|
||||
description: Makex fixes to the build environment
|
||||
|
||||
inputs:
|
||||
company:
|
||||
description: Company whose build to fix
|
||||
title:
|
||||
description: Company whose release to release
|
||||
envPath:
|
||||
description: Path to the fastlane environment file
|
||||
provisioningProfileTeam:
|
||||
description: Name of provisioning profile for the base app
|
||||
provisioningProfileNotify:
|
||||
description: Name of provisioning profile for the notification
|
||||
provisioningProfileShare:
|
||||
description: Name of provisioning profile for the sharing
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: ci/fix-ios-build
|
||||
shell: bash
|
||||
# WARNING: we are using BSD sed here and that requires -i ''
|
||||
run: |
|
||||
sudo mkdir /Users/runner
|
||||
sudo chown mac /Users/runner
|
||||
sudo mkdir /opt/hostedtoolcache
|
||||
sudo chown mac /opt/hostedtoolcache
|
||||
sed -i '' -E "s:^(SENTRY_ENABLED\=)(.*)$:\1false:" "${{ inputs.envPath }}"
|
||||
sed -i '' -E "s:^(APP_NAME\=)(.*)$:\1${{ inputs.title }}:" "${{ inputs.envPath }}"
|
||||
sed -i '' -E "s:^(APP_SCHEME\=)(.*)$:\1mm\-${{ inputs.company }}:" "${{ inputs.envPath }}"
|
||||
sed -i '' -E "s:^(FASTLANE_TEAM_ID\=)(.*)$:\1:" "${{ inputs.envPath }}"
|
||||
sed -i '' -E "s:^(MAIN_APP_IDENTIFIER\=)(.*)$:\1cz.${{ inputs.company }}.team:" "${{ inputs.envPath }}"
|
||||
sed -i '' -E "s:^(NOTIFICATION_SERVICE_IDENTIFIER\=)(.*)$:\1cz.${{ inputs.company }}.team.NotificationService:" "${{ inputs.envPath }}"
|
||||
sed -i '' -E "s:^(EXTENSION_APP_IDENTIFIER\=)(.*)$:\1cz.${{ inputs.company }}.team.MattermostShare:" "${{ inputs.envPath }}"
|
||||
sed -i '' -E "s:^(IOS_APP_GROUP\=)(.*)$:\1group.cz.${{ inputs.company }}.team:" "${{ inputs.envPath }}"
|
||||
sed -i '' -E "s:^(IOS_ICLOUD_CONTAINER\=)(.*)$:\1iCloud.cz.${{ inputs.company }}.team:" "${{ inputs.envPath }}"
|
||||
sed -i '' -E "s:^(MATCH_APP_IDENTIFIER\=)(.*)$:\1cz.${{ inputs.company }}.team,cz.${{ inputs.company }}.team.NotificationService,cz.${{ inputs.company }}.team.MattermostShare:" "${{ inputs.envPath }}"
|
||||
sed -i '' -E "s:^([[:space:]]*)(.*)(Aws\:\:S3\:\:Resource\.new)(.*)$:\1Aws.config.update\(endpoint\:'http\://storage.cluster\:81',force_path_style\:true\)\n\1\2\3\4:" fastlane/Fastfile
|
||||
sed -i '' -E "s:^([[:space:]]*export_options\:[[:space:]]*\{)(.*)$:\1provisioningProfiles\:\{'cz.${{ inputs.company }}.team'\:'${{ inputs.provisioningProfileTeam }}','cz.${{ inputs.company }}.team.NotificationService'\:'${{ inputs.provisioningProfileNotify }}','cz.${{ inputs.company }}.team.MattermostShare'\:'${{ inputs.provisioningProfileShare }}'\},\2:" fastlane/Fastfile
|
||||
echo Hack 'detected dubious ownership in repository' while aplying patches
|
||||
HOME=/Users/mac git config --global --add safe.directory /Users/mac/drone/src
|
||||
@@ -1,26 +0,0 @@
|
||||
name: droid-install-ios-profile
|
||||
description: Installs a provisioning profile
|
||||
|
||||
inputs:
|
||||
company:
|
||||
description: Company whose profile to install
|
||||
default: "exprojekt"
|
||||
name:
|
||||
description: Profile file name
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: ci/install-profile
|
||||
shell: bash
|
||||
env:
|
||||
#PROFILES_PATH: "/private/var/root/Library/MobileDevice/Provisioning Profiles"
|
||||
PROFILES_PATH: "/Users/mac/Library/MobileDevice/Provisioning Profiles"
|
||||
run: |
|
||||
mkdir -p "$PROFILES_PATH"
|
||||
srcPath=${{ github.workspace }}/mattermost-mobile-private/ios/${{ inputs.company }}/${{ inputs.name }}
|
||||
uuid=`grep UUID -A1 -a "$srcPath" | grep -io "[-A-F0-9]\{36\}"`
|
||||
name=${{ inputs.name }}
|
||||
extension="${name##*.}"
|
||||
echo Installing provisioning profile $name -> $uuid
|
||||
cp "$srcPath" "$PROFILES_PATH/${uuid}.${extension}"
|
||||
114
.eslintrc.json
Normal file
114
.eslintrc.json
Normal file
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"extends": [
|
||||
"./eslint/eslint-mattermost",
|
||||
"./eslint/eslint-react",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"import"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"pragma": "React",
|
||||
"version": "17.0"
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
"globals": {
|
||||
"__DEV__": true
|
||||
},
|
||||
"rules": {
|
||||
"eol-last": ["error", "always"],
|
||||
"global-require": 0,
|
||||
"no-undefined": 0,
|
||||
"no-shadow": "off",
|
||||
"react/display-name": [2, { "ignoreTranspilerName": false }],
|
||||
"react/jsx-filename-extension": 0,
|
||||
"react-hooks/exhaustive-deps": 0,
|
||||
"camelcase": [
|
||||
0,
|
||||
{
|
||||
"properties": "never"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/ban-types": 0,
|
||||
"@typescript-eslint/no-non-null-assertion": 0,
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
2,
|
||||
{
|
||||
"vars": "all",
|
||||
"args": "after-used"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-shadow": ["error"],
|
||||
"@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",
|
||||
"no-underscore-dangle": "off",
|
||||
"indent": [2, 4, {"SwitchCase": 1}],
|
||||
"key-spacing": [2, {
|
||||
"singleLine": {
|
||||
"beforeColon": false,
|
||||
"afterColon": true
|
||||
}}],
|
||||
"@typescript-eslint/member-delimiter-style": 2,
|
||||
"import/order": [
|
||||
2,
|
||||
{
|
||||
"groups": ["builtin", "external", "parent", "sibling", "index", "type"],
|
||||
"newlines-between": "always",
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "{@(@actions|@app|@assets|@calls|@client|@components|@constants|@context|@database|@helpers|@hooks|@init|@managers|@queries|@screens|@selectors|@share|@store|@telemetry|@typings|@test|@utils)/**,@(@constants|@i18n|@notifications|@store|@websocket)}",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "app/**",
|
||||
"group": "parent",
|
||||
"position": "before"
|
||||
}
|
||||
],
|
||||
"alphabetize": {
|
||||
"order": "asc",
|
||||
"caseInsensitive": true
|
||||
},
|
||||
"pathGroupsExcludedImportTypes": ["type"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.test.js", "*.test.jsx"],
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["detox/e2e/**"],
|
||||
"globals": {
|
||||
"by": true,
|
||||
"detox": true,
|
||||
"device": true,
|
||||
"element": true,
|
||||
"waitFor": true
|
||||
},
|
||||
"rules": {
|
||||
"func-names": 0,
|
||||
"import/no-unresolved": 0,
|
||||
"max-nested-callbacks": 0,
|
||||
"no-process-env": 0,
|
||||
"no-unused-expressions": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -27,7 +27,6 @@ Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fie
|
||||
- [ ] Has UI changes
|
||||
- [ ] Includes text changes and localization file updates
|
||||
- [ ] Have tested against the 5 core themes to ensure consistency between them.
|
||||
- [ ] Have run E2E tests by adding label `E2E iOS tests for PR`.
|
||||
|
||||
#### Device Information
|
||||
This PR was tested on: <!-- Device name(s), OS version(s) -->
|
||||
|
||||
71
.github/actions/bandwidth-throttling/action.yml
vendored
71
.github/actions/bandwidth-throttling/action.yml
vendored
@@ -1,71 +0,0 @@
|
||||
name: bandwidth-throttling
|
||||
description: Action to throttle the bandwidth on MacOS runner
|
||||
|
||||
inputs:
|
||||
test_server_host:
|
||||
description: The host of the test server, no protocol
|
||||
required: true
|
||||
download_speed:
|
||||
description: The download speed limit (in Kbit/s)
|
||||
required: false
|
||||
default: "3300"
|
||||
upload_speed:
|
||||
description: The upload speed limit (in Kbit/s)
|
||||
required: false
|
||||
default: "3300"
|
||||
latency:
|
||||
description: The latency (in ms) each way
|
||||
required: false
|
||||
default: "500"
|
||||
disable:
|
||||
description: Disable throttling
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: disable first
|
||||
if: ${{ inputs.disable == 'true' }}
|
||||
shell: bash
|
||||
continue-on-error: true
|
||||
run: |
|
||||
sudo pfctl -d
|
||||
|
||||
sleep 2;
|
||||
|
||||
- name: throttle bandwidth down
|
||||
shell: bash
|
||||
run: |
|
||||
# reset pf and dnctl
|
||||
sudo dnctl -q flush
|
||||
sudo dnctl -q pipe flush
|
||||
sudo pfctl -f /etc/pf.conf
|
||||
sudo pfctl -E
|
||||
|
||||
sleep 2;
|
||||
|
||||
sudo pfctl -d
|
||||
sudo dnctl -q flush
|
||||
sudo dnctl -q pipe flush
|
||||
|
||||
|
||||
echo "dummynet in from ${{ inputs.test_server_host }} to ! 127.0.0.1 pipe 1
|
||||
dummynet out from ! 127.0.0.1 to ${{ inputs.test_server_host }} pipe 2" | sudo pfctl -f -
|
||||
|
||||
# pipe 1 is download
|
||||
sudo dnctl pipe 1 config bw ${{ inputs.download_speed }}Kbit/s delay ${{ inputs.latency }}ms
|
||||
|
||||
# pipe 2 is upload
|
||||
sudo dnctl pipe 2 config bw ${{ inputs.upload_speed }}Kbit/s delay ${{ inputs.latency }}ms
|
||||
|
||||
sleep 5;
|
||||
|
||||
sudo pfctl -E
|
||||
|
||||
sleep 5;
|
||||
|
||||
- name: test curl after throttling
|
||||
shell: bash
|
||||
run: |
|
||||
curl -o /dev/null -m 20 --retry 2 -s -w 'Total: %{time_total}s\n' 'https://${{ inputs.test_server_host }}/api/v4/system/ping?get_server_status=true'
|
||||
39
.github/actions/generate-specs/action.yml
vendored
39
.github/actions/generate-specs/action.yml
vendored
@@ -1,39 +0,0 @@
|
||||
# Copyright 2024 Mattermost, Inc.
|
||||
name: "generate-specs"
|
||||
description: This action used to split Detox integration tests based on the parallelism provided
|
||||
|
||||
inputs:
|
||||
search_path:
|
||||
description: The path to look for from within the directory
|
||||
required: true
|
||||
parallelism:
|
||||
description: The parallelism for the tests
|
||||
required: true
|
||||
device_name:
|
||||
description: The name of Device used for the tests
|
||||
required: false
|
||||
default: "iPhone 15"
|
||||
device_os_version:
|
||||
description: The os of the device used for the tests
|
||||
required: false
|
||||
default: "iOS 17.1"
|
||||
|
||||
outputs:
|
||||
specs:
|
||||
description: The specs generated for the strategy
|
||||
value: ${{ steps.generate-specs.outputs.specs }}
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: ci/generate-specs
|
||||
id: generate-specs
|
||||
env:
|
||||
PARALLELISM: ${{ inputs.parallelism }}
|
||||
SEARCH_PATH: ${{ inputs.search_path }}
|
||||
DEVICE_NAME: ${{ inputs.device_name }}
|
||||
DEVICE_OS_VERSION: ${{ inputs.device_os_version }}
|
||||
run: |
|
||||
set -e
|
||||
node ${{ github.action_path }}/split-tests.js | tee output.json
|
||||
echo "specs=$(cat output.json)" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
96
.github/actions/generate-specs/split-tests.js
vendored
96
.github/actions/generate-specs/split-tests.js
vendored
@@ -1,96 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class DeviceInfo {
|
||||
constructor(deviceName, deviceOsVersion) {
|
||||
this.deviceName = deviceName;
|
||||
this.deviceOsVersion = deviceOsVersion;
|
||||
}
|
||||
}
|
||||
|
||||
class SpecGroup {
|
||||
constructor(runId, specs, deviceInfo) {
|
||||
this.runId = runId;
|
||||
this.specs = specs;
|
||||
this.deviceName = deviceInfo.deviceName;
|
||||
this.deviceOsVersion = deviceInfo.deviceOsVersion;
|
||||
}
|
||||
}
|
||||
|
||||
class Specs {
|
||||
constructor(searchPath, parallelism, deviceInfo) {
|
||||
this.searchPath = searchPath;
|
||||
this.parallelism = parallelism;
|
||||
this.rawFiles = [];
|
||||
this.groupedFiles = [];
|
||||
this.deviceInfo = deviceInfo;
|
||||
}
|
||||
|
||||
findFiles() {
|
||||
const dirPath = path.join(this.searchPath);
|
||||
|
||||
const fileRegex = /\.e2e\.ts$/;
|
||||
|
||||
const walkSync = (currentPath) => {
|
||||
const files = fs.readdirSync(currentPath);
|
||||
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(currentPath, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
walkSync(filePath);
|
||||
} else if (fileRegex.test(filePath)) {
|
||||
const relativeFilePath = filePath.replace(dirPath + '/', '');
|
||||
const fullPath = path.join(this.searchPath, relativeFilePath);
|
||||
this.rawFiles.push(fullPath);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
walkSync(dirPath);
|
||||
}
|
||||
|
||||
generateSplits() {
|
||||
const chunkSize = Math.floor(this.rawFiles.length / this.parallelism);
|
||||
let remainder = this.rawFiles.length % this.parallelism;
|
||||
let runNo = 1;
|
||||
let start = 0;
|
||||
|
||||
for (let i = 0; i < this.parallelism; i++) {
|
||||
let end = start + chunkSize + (remainder > 0 ? 1 : 0);
|
||||
const fileGroup = this.rawFiles.slice(start, end).join(' ');
|
||||
const specFileGroup = new SpecGroup(runNo.toString(), fileGroup, this.deviceInfo);
|
||||
this.groupedFiles.push(specFileGroup);
|
||||
|
||||
start = end;
|
||||
runNo++;
|
||||
if (remainder > 0) {
|
||||
remainder--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dumpSplits() {
|
||||
const output = {
|
||||
include: this.groupedFiles,
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(output));
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const searchPath = process.env.SEARCH_PATH;
|
||||
const parallelism = parseInt(process.env.PARALLELISM, 10);
|
||||
const deviceName = process.env.DEVICE_NAME;
|
||||
const deviceOsVersion = process.env.DEVICE_OS_VERSION;
|
||||
const deviceInfo = new DeviceInfo(deviceName, deviceOsVersion);
|
||||
const specs = new Specs(searchPath, parallelism, deviceInfo);
|
||||
|
||||
specs.findFiles();
|
||||
specs.generateSplits();
|
||||
specs.dumpSplits();
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,46 +0,0 @@
|
||||
name: prepare-android-build
|
||||
description: Action to prepare environment for android build
|
||||
|
||||
inputs:
|
||||
sign:
|
||||
description: Flag to enable android package signing
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: ci/prepare-mobile-build
|
||||
uses: ./.github/actions/prepare-mobile-build
|
||||
|
||||
- name: ci/setup-java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: ci/jetify-android-libraries
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::jetify-android-libraries"
|
||||
./node_modules/.bin/jetify
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: ci/checkout-private-repo
|
||||
if: ${{ inputs.sign == 'neverMatches' }}
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
repository: mattermost/mattermost-mobile-private
|
||||
token: ${{ env.MATTERMOST_BUILD_GH_TOKEN }}
|
||||
path: ${{ github.workspace }}/mattermost-mobile-private
|
||||
|
||||
- name: ci/append-keystore-to-android-build-for-signing
|
||||
if: ${{ inputs.sign == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::append-keystore-to-android-build-for-signing"
|
||||
cp ${{ github.workspace }}/mattermost-mobile-private/android/${STORE_FILE} android/app/${STORE_FILE}
|
||||
echo "" | tee -a android/gradle.properties > /dev/null
|
||||
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
|
||||
echo "::endgroup::"
|
||||
29
.github/actions/prepare-ios-build/action.yaml
vendored
29
.github/actions/prepare-ios-build/action.yaml
vendored
@@ -1,29 +0,0 @@
|
||||
name: prepare-ios-build
|
||||
description: Action to prepare environment for ios build
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: ci/setup-xcode
|
||||
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
|
||||
with:
|
||||
xcode-version: latest-stable
|
||||
|
||||
- name: ci/prepare-mobile-build
|
||||
uses: ./.github/actions/prepare-mobile-build
|
||||
|
||||
- name: ci/install-pods-dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::install-pods-dependencies"
|
||||
npm run ios-gems
|
||||
npm run pod-install
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: Cache Pods
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: Pods
|
||||
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pods-
|
||||
106
.github/actions/prepare-low-bandwidth/action.yml
vendored
106
.github/actions/prepare-low-bandwidth/action.yml
vendored
@@ -1,106 +0,0 @@
|
||||
name: Prepare Low Bandwidth Environment (MacOS & iOS Simulators only)
|
||||
description: prepare any workflow for low bandwidth testing
|
||||
|
||||
inputs:
|
||||
test_server_url:
|
||||
description: The URL of the test server
|
||||
required: true
|
||||
device_name:
|
||||
description: The iOS simulator name
|
||||
required: true
|
||||
download_speed:
|
||||
description: The download speed limit (in Kbit/s)
|
||||
required: false
|
||||
default: "3300"
|
||||
upload_speed:
|
||||
description: The upload speed limit (in Kbit/s)
|
||||
required: false
|
||||
default: "3300"
|
||||
latency:
|
||||
description: The latency (in ms) each way
|
||||
required: false
|
||||
default: "500"
|
||||
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: delete the zip file and trash (to free up space)
|
||||
shell: bash
|
||||
run: |
|
||||
rm -rf mobile-artifacts/*.zip
|
||||
sudo rm -rf ~/.Trash/*
|
||||
|
||||
- name: check disk space
|
||||
shell: bash
|
||||
run: df -h
|
||||
|
||||
- name: remove protocol from SITE_1_URL
|
||||
id: remove-protocol
|
||||
shell: bash
|
||||
run: |
|
||||
echo "SITE_1_HOST=${{ inputs.test_server_url }}" | sed -e 's/http:\/\///g' -e 's/https:\/\///g' >> ${GITHUB_OUTPUT}
|
||||
|
||||
- name: Throttle Bandwidth 1
|
||||
id: throttle-bandwidth-1
|
||||
continue-on-error: true
|
||||
uses: ./.github/actions/bandwidth-throttling
|
||||
with:
|
||||
test_server_host: ${{ steps.remove-protocol.outputs.SITE_1_HOST }}
|
||||
download_speed: ${{ inputs.download_speed }}
|
||||
upload_speed: ${{ inputs.upload_speed }}
|
||||
latency: ${{ inputs.latency }}
|
||||
|
||||
- name: Throttle Bandwidth 2
|
||||
if: steps.throttle-bandwidth-1.outcome != 'success'
|
||||
id: throttle-bandwidth-2
|
||||
uses: ./.github/actions/bandwidth-throttling
|
||||
with:
|
||||
test_server_host: ${{ steps.remove-protocol.outputs.SITE_1_HOST }}
|
||||
download_speed: ${{ inputs.download_speed}}
|
||||
upload_speed: ${{ inputs.upload_speed }}
|
||||
latency: ${{ inputs.latency }}
|
||||
disable: "true"
|
||||
|
||||
- name: Install mitmproxy & pm2 (process manager)
|
||||
id: install-mitmproxy-pm2
|
||||
shell: bash
|
||||
run: |
|
||||
brew install mitmproxy
|
||||
|
||||
npm i -g pm2
|
||||
|
||||
- name: Start mitmproxy via mitmdump and stop it (to get .mitmproxy folder)
|
||||
shell: bash
|
||||
run: |
|
||||
pm2 start mitmdump --log /Users/runner/work/mattermost-mobile/mattermost-mobile/mitmdump.log -- --allow-hosts '${{ steps.remove-protocol.outputs.SITE_1_HOST }}' --ignore-hosts 'localhost' -s /Users/runner/work/mattermost-mobile/mattermost-mobile/scripts/mitmdump-flow-parsing.py
|
||||
|
||||
sleep 5;
|
||||
|
||||
pm2 stop mitmdump
|
||||
|
||||
# we need to wait for mitmdump to stop so it'll produce the .mitmproxy folder
|
||||
sleep 5;
|
||||
|
||||
- name: Get simulator UDID
|
||||
id: get-simulator-udid
|
||||
shell: bash
|
||||
run: |
|
||||
simulator_udid=$(xcrun simctl list devices "${{ inputs.device_name }}" -j | jq '.devices' | jq '."com.apple.CoreSimulator.SimRuntime.iOS-17-4"[0]["udid"]')
|
||||
echo "simulator_udid="$(echo $simulator_udid) >> ${GITHUB_OUTPUT}
|
||||
|
||||
- name: install certificate
|
||||
shell: bash
|
||||
run: |
|
||||
sudo security add-trusted-cert -d -p ssl -p basic -k /Library/Keychains/System.keychain ~/.mitmproxy/mitmproxy-ca-cert.pem
|
||||
|
||||
# must boot first before adding root cert
|
||||
xcrun simctl boot ${{ steps.get-simulator-udid.outputs.simulator_udid }}
|
||||
|
||||
xcrun simctl keychain booted add-root-cert ~/.mitmproxy/mitmproxy-ca-cert.pem
|
||||
|
||||
sleep 5;
|
||||
|
||||
- name: show me booted simulators
|
||||
shell: bash
|
||||
run: xcrun simctl list devices booted | grep Booted
|
||||
27
.github/actions/prepare-mobile-build/action.yaml
vendored
27
.github/actions/prepare-mobile-build/action.yaml
vendored
@@ -1,27 +0,0 @@
|
||||
name: prepare-mobile-build
|
||||
description: Action to prepare environment for mobile build
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# The required ruby version is mentioned in '.ruby-version'
|
||||
- uses: ruby/setup-ruby@cb0fda56a307b8c78d38320cd40d9eb22a3bf04e # v1.242.0
|
||||
|
||||
- name: ci/setup-fastlane-dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::setup-fastlane-dependencies"
|
||||
bundle install
|
||||
echo "::endgroup::"
|
||||
working-directory: ./fastlane
|
||||
|
||||
- name: Cache Ruby gems
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gems-
|
||||
|
||||
- name: ci/prepare-node-deps
|
||||
uses: ./.github/actions/prepare-node-deps
|
||||
54
.github/actions/prepare-node-deps/action.yaml
vendored
54
.github/actions/prepare-node-deps/action.yaml
vendored
@@ -1,54 +0,0 @@
|
||||
name: deps
|
||||
description: Common deps for mobile repo
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: ci/setup-node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "npm"
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: ci/install-npm-dependencies
|
||||
shell: bash
|
||||
env:
|
||||
NODE_ENV: development
|
||||
run: |
|
||||
echo "::group::install-npm-dependencies"
|
||||
npm ci --ignore-scripts
|
||||
node node_modules/\@sentry/cli/scripts/install.js
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: ci/patch-npm-dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::patch-npm-dependencies"
|
||||
npx patch-package
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: ci/generate-assets
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::generate-assets"
|
||||
node ./scripts/generate-assets.js
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: ci/import-compass-icon
|
||||
shell: bash
|
||||
env:
|
||||
COMPASS_ICONS: "node_modules/@mattermost/compass-icons/font/compass-icons.ttf"
|
||||
run: |
|
||||
echo "::group::import-compass-icon"
|
||||
cp "$COMPASS_ICONS" "assets/fonts/"
|
||||
cp "$COMPASS_ICONS" "android/app/src/main/assets/fonts"
|
||||
echo "::endgroup::"
|
||||
36
.github/actions/start-proxy/action.yml
vendored
36
.github/actions/start-proxy/action.yml
vendored
@@ -1,36 +0,0 @@
|
||||
name: Start Proxy
|
||||
description: Action to throttle the bandwidth on MacOS runner
|
||||
|
||||
inputs:
|
||||
test_server_url:
|
||||
description: The host of the test server, no protocol
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
|
||||
- name: restart mitmdump
|
||||
shell: bash
|
||||
run: |
|
||||
pm2 restart mitmdump
|
||||
|
||||
sleep 5;
|
||||
|
||||
- name: start proxy
|
||||
shell: bash
|
||||
run: |
|
||||
networksetup -setwebproxy Ethernet "127.0.0.1" "8080"
|
||||
networksetup -setsecurewebproxy Ethernet "127.0.0.1" "8080"
|
||||
|
||||
sleep 5;
|
||||
|
||||
networksetup -getwebproxy Ethernet
|
||||
networksetup -getsecurewebproxy Ethernet
|
||||
|
||||
- name: test curl and direct it into proxy
|
||||
shell: bash
|
||||
run: |
|
||||
curl -o /dev/null -m 20 -s -w 'Total: %{time_total}s\n' '${{ inputs.test_server_url }}/api/v4/system/ping?get_server_status=true'
|
||||
|
||||
curl --proxy "127.0.0.1:8080" -o /dev/null -m 20 -s -w 'Total: %{time_total}s\n' '${{ inputs.test_server_url }}/api/v4/system/ping?get_server_status=true'
|
||||
146
.github/actions/test-coverage/action.yml
vendored
146
.github/actions/test-coverage/action.yml
vendored
@@ -1,146 +0,0 @@
|
||||
# This action is used to test the coverage of the mobile repo
|
||||
# It will download the coverage result from the main branch and compare it with the current branch
|
||||
# If the coverage is lower than the main branch (1% or more), it will post a warning along with
|
||||
# the coverage report to the PR.
|
||||
# If this action is run on the main branch, it will upload the coverage result to the main branch
|
||||
# It will also generate a run id and cache it, so that the next time the action is run in the PR,
|
||||
# it will use the cached run id to download the coverage result from the main branch
|
||||
|
||||
name: test-coverage
|
||||
description: Test coverage tracking for mobile repo
|
||||
|
||||
inputs:
|
||||
run_id:
|
||||
description: The run id to use to download the coverage result
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: ci/prepare-node-deps
|
||||
uses: ./.github/actions/prepare-node-deps
|
||||
|
||||
- name: ci/get-last-run-id
|
||||
if: github.event_name == 'pull_request'
|
||||
id: get-last-run-id
|
||||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: run-id.txt
|
||||
key: last-run-id-${{ inputs.run_id }}
|
||||
restore-keys: |
|
||||
last-run-id-
|
||||
|
||||
- name: ci/set-pr-condition
|
||||
if: github.event_name == 'pull_request'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::set-pr-condition"
|
||||
if [ -f "run-id.txt" ]; then
|
||||
echo "HAS_MAIN_RUN_ID=true" >> $GITHUB_ENV
|
||||
echo "LAST_RUN_ID=$(cat run-id.txt)" >> $GITHUB_ENV
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: ci/download-main-coverage
|
||||
if: env.HAS_MAIN_RUN_ID == 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: test-coverage-result-${{ env.LAST_RUN_ID }}
|
||||
path: main-coverage/
|
||||
github-token: ${{ github.token }}
|
||||
run-id: ${{ env.LAST_RUN_ID }}
|
||||
|
||||
- name: ci/check-coverage-download
|
||||
if: env.HAS_MAIN_RUN_ID == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::check-coverage-download"
|
||||
if [ -f "main-coverage/coverage-summary.json" ]; then
|
||||
echo "HAS_COVERAGE_FROM_MAIN=true" >> $GITHUB_ENV
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: ci/read-coverage
|
||||
if: env.HAS_COVERAGE_FROM_MAIN == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::read-coverage"
|
||||
./scripts/read-coverage.sh ./main-coverage/coverage-summary.json
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: ci/run-tests-with-coverage
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::run-tests"
|
||||
npm run test:ci:coverage
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: ci/compare-coverage
|
||||
if: env.HAS_COVERAGE_FROM_MAIN == 'true'
|
||||
id: compare-coverage
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::compare-coverage"
|
||||
echo "IS_FORK=false" >> $GITHUB_ENV
|
||||
output=$(./scripts/compare-coverage.sh \
|
||||
./main-coverage \
|
||||
./coverage \
|
||||
${{ github.event.pull_request.number }})
|
||||
echo "report<<EOF" >> $GITHUB_ENV
|
||||
echo "$output" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
if [ "${{ github.event.pull_request.head.repo.fork }}" = "true" ]; then
|
||||
echo "IS_FORK=true" >> $GITHUB_ENV
|
||||
echo "::warning::PR is from a fork. Coverage report will be displayed in the workflow summary."
|
||||
echo "### Coverage Report from Fork" >> $GITHUB_STEP_SUMMARY
|
||||
echo "$output" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: ci/post-coverage-report
|
||||
if: env.HAS_COVERAGE_FROM_MAIN == 'true' && env.IS_FORK == 'false'
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
message: ${{ env.report }}
|
||||
comment-tag: coverage-report
|
||||
create-if-not-exists: true
|
||||
|
||||
- name: ci/exit-on-test-coverage-failure
|
||||
if: env.HAS_COVERAGE_FROM_MAIN == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::exit-on-test-coverage-failure"
|
||||
exit ${{ steps.compare-coverage.outputs.status }}
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: ci/upload-coverage
|
||||
if: github.ref_name == 'main'
|
||||
id: upload-coverage
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-coverage-result-${{ inputs.run_id }}
|
||||
path: coverage/coverage-summary.json
|
||||
overwrite: true
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: ci/set-upload-success
|
||||
if: github.ref_name == 'main' && steps.upload-coverage.outcome == 'success'
|
||||
shell: bash
|
||||
run: echo "UPLOAD_SUCCESS=true" >> $GITHUB_ENV
|
||||
|
||||
- name: ci/generate-run-id-file
|
||||
if: env.UPLOAD_SUCCESS == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::generate-last-run-id"
|
||||
echo "${{ inputs.run_id }}" > run-id.txt
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: ci/cache-run-id-file
|
||||
if: env.UPLOAD_SUCCESS == 'true'
|
||||
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
|
||||
with:
|
||||
path: run-id.txt
|
||||
key: last-run-id-${{ inputs.run_id }}
|
||||
31
.github/actions/test/action.yaml
vendored
31
.github/actions/test/action.yaml
vendored
@@ -1,31 +0,0 @@
|
||||
name: test
|
||||
description: Common tests for mobile repo
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: ci/prepare-node-deps
|
||||
uses: ./.github/actions/prepare-node-deps
|
||||
|
||||
- name: ci/check-styles
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::check-styles"
|
||||
npm run check
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: ci/run-tests
|
||||
# main and PR will run test:coverage
|
||||
if: startsWith(github.ref_name, 'release-')
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::run-tests"
|
||||
npm run test:ci
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: ci/check-i18n
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::check-i18n"
|
||||
./scripts/precommit/i18n.sh
|
||||
echo "::endgroup::"
|
||||
18
.github/dependabot.yaml
vendored
18
.github/dependabot.yaml
vendored
@@ -1,18 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directories:
|
||||
- "/.github/workflows"
|
||||
- "/.github/actions/**/*"
|
||||
reviewers:
|
||||
- "mattermost/core-build-engineers"
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
Github Actions updates:
|
||||
applies-to: version-updates
|
||||
dependency-type: production
|
||||
schedule:
|
||||
# Check for updates to GitHub Actions every week
|
||||
day: "monday"
|
||||
time: "09:00"
|
||||
interval: "weekly"
|
||||
60
.github/workflows/build-android-beta.yml
vendored
60
.github/workflows/build-android-beta.yml
vendored
@@ -1,60 +0,0 @@
|
||||
---
|
||||
name: build-android-beta
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- build-beta-[0-9]+
|
||||
- build-beta-android-[0-9]+
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.14.0
|
||||
TERM: xterm
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
||||
build-and-deploy-android-beta:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/prepare-android-build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
env:
|
||||
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
|
||||
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
|
||||
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
|
||||
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
|
||||
|
||||
- name: ci/build-and-deploy-android-beta
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
|
||||
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_BETA_MATTERMOST_WEBHOOK_URL }}"
|
||||
SENTRY_AUTH_TOKEN: "${{ secrets.MM_MOBILE_SENTRY_AUTH_TOKEN }}"
|
||||
SENTRY_DSN_ANDROID: ${{ secrets.MM_MOBILE_BETA_SENTRY_DSN_ANDROID }}
|
||||
SUPPLY_JSON_KEY: ${{ github.workspace }}/mattermost-mobile-private/android/mattermost-credentials.json
|
||||
run: |
|
||||
echo "::group::Build"
|
||||
bundle exec fastlane android build --env android.beta
|
||||
echo "::endgroup::"
|
||||
echo "::group::Deploy to Play Store"
|
||||
bundle exec fastlane android deploy file:"${{ github.workspace }}/*.apk" --env android.beta
|
||||
echo "::endgroup::"
|
||||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-android-beta-build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: android-build-beta-${{ github.run_id }}
|
||||
path: "*.apk"
|
||||
60
.github/workflows/build-android-release.yml
vendored
60
.github/workflows/build-android-release.yml
vendored
@@ -1,60 +0,0 @@
|
||||
---
|
||||
name: build-android-release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- build-release-[0-9]+
|
||||
- build-release-android-[0-9]+
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.14.0
|
||||
TERM: xterm
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
||||
build-and-deploy-android-release:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/prepare-android-build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
env:
|
||||
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
|
||||
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
|
||||
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
|
||||
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
|
||||
|
||||
- name: ci/build-and-deploy-android-release
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_RELEASE_AWS_ACCESS_KEY_ID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_RELEASE_AWS_SECRET_ACCESS_KEY }}"
|
||||
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_RELEASE_MATTERMOST_WEBHOOK_URL }}"
|
||||
SENTRY_AUTH_TOKEN: "${{ secrets.MM_MOBILE_SENTRY_AUTH_TOKEN }}"
|
||||
SENTRY_DSN_ANDROID: ${{ secrets.MM_MOBILE_RELEASE_SENTRY_DSN_ANDROID }}
|
||||
SUPPLY_JSON_KEY: ${{ github.workspace }}/mattermost-mobile-private/android/mattermost-credentials.json
|
||||
run: |
|
||||
echo "::group::Build"
|
||||
bundle exec fastlane android build --env android.release
|
||||
echo "::endgroup::"
|
||||
echo "::group::Deploy to Play Store"
|
||||
bundle exec fastlane android deploy file:"${{ github.workspace }}/*.apk" --env android.release
|
||||
echo "::endgroup::"
|
||||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-android-release-build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: android-build-release-${{ github.run_id }}
|
||||
path: "*.apk"
|
||||
99
.github/workflows/build-ios-beta.yml
vendored
99
.github/workflows/build-ios-beta.yml
vendored
@@ -1,99 +0,0 @@
|
||||
---
|
||||
name: build-ios-beta
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- build-beta-[0-9]+
|
||||
- build-beta-ios-[0-9]+
|
||||
- build-beta-sim-[0-9]+
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.14.0
|
||||
TERM: xterm
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
||||
build-ios-simulator:
|
||||
runs-on: macos-14-large
|
||||
if: ${{ !contains(github.ref_name, 'beta-ios') }}
|
||||
needs:
|
||||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
||||
- name: ci/build-ios-simulator
|
||||
env:
|
||||
TAG: "${{ github.ref_name }}"
|
||||
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
|
||||
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_BETA_MATTERMOST_WEBHOOK_URL }}"
|
||||
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
|
||||
run: bundle exec fastlane ios simulator --env ios.simulator
|
||||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-ios-pr-simulator
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ios-build-simulator-${{ github.run_id }}
|
||||
path: Mattermost-simulator-x86_64.app.zip
|
||||
|
||||
build-and-deploy-ios-beta:
|
||||
runs-on: macos-14-large
|
||||
if: ${{ !contains(github.ref_name, 'beta-sim') }}
|
||||
needs:
|
||||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/output-ssh-private-key
|
||||
shell: bash
|
||||
run: |
|
||||
SSH_KEY_PATH=~/.ssh/id_ed25519
|
||||
mkdir -p ~/.ssh
|
||||
echo -e '${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}' > ${SSH_KEY_PATH}
|
||||
chmod 0600 ${SSH_KEY_PATH}
|
||||
ssh-keygen -y -f ${SSH_KEY_PATH} > ${SSH_KEY_PATH}.pub
|
||||
|
||||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
||||
- name: ci/build-and-deploy-ios-beta
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
|
||||
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_BETA_MATTERMOST_WEBHOOK_URL }}"
|
||||
FASTLANE_TEAM_ID: "${{ secrets.MM_MOBILE_FASTLANE_TEAM_ID }}"
|
||||
IOS_API_ISSUER_ID: "${{ secrets.MM_MOBILE_IOS_API_ISSUER_ID }}"
|
||||
IOS_API_KEY: "${{ secrets.MM_MOBILE_IOS_API_KEY }}"
|
||||
IOS_API_KEY_ID: "${{ secrets.MM_MOBILE_IOS_API_KEY_ID }}"
|
||||
MATCH_GIT_URL: "${{ secrets.MM_MOBILE_MATCH_GIT_URL }}"
|
||||
MATCH_PASSWORD: "${{ secrets.MM_MOBILE_MATCH_PASSWORD }}"
|
||||
SENTRY_AUTH_TOKEN: "${{ secrets.MM_MOBILE_SENTRY_AUTH_TOKEN }}"
|
||||
SENTRY_DSN_IOS: "${{ secrets.MM_MOBILE_BETA_SENTRY_DSN_IOS }}"
|
||||
run: |
|
||||
echo "::group::Build"
|
||||
bundle exec fastlane ios build --env ios.beta
|
||||
echo "::endgroup::"
|
||||
echo "::group::Deploy to TestFlight"
|
||||
bundle exec fastlane ios deploy file:"${{ github.workspace }}/*.ipa" --env ios.beta
|
||||
echo "::endgroup::"
|
||||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-ios-beta-build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ios-build-beta-${{ github.run_id }}
|
||||
path: "*.ipa"
|
||||
99
.github/workflows/build-ios-release.yml
vendored
99
.github/workflows/build-ios-release.yml
vendored
@@ -1,99 +0,0 @@
|
||||
---
|
||||
name: build-ios-release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- build-release-[0-9]+
|
||||
- build-release-ios-[0-9]+
|
||||
- build-release-sim-[0-9]+
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.14.0
|
||||
TERM: xterm
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
||||
build-and-deploy-ios-release:
|
||||
runs-on: macos-14-large
|
||||
if: ${{ !contains(github.ref_name, 'release-sim') }}
|
||||
needs:
|
||||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
||||
- name: ci/output-ssh-private-key
|
||||
shell: bash
|
||||
run: |
|
||||
SSH_KEY_PATH=~/.ssh/id_ed25519
|
||||
mkdir -p ~/.ssh
|
||||
echo -e '${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}' > ${SSH_KEY_PATH}
|
||||
chmod 0600 ${SSH_KEY_PATH}
|
||||
ssh-keygen -y -f ${SSH_KEY_PATH} > ${SSH_KEY_PATH}.pub
|
||||
|
||||
- name: ci/build-and-deploy-ios-release
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_RELEASE_AWS_ACCESS_KEY_ID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_RELEASE_AWS_SECRET_ACCESS_KEY }}"
|
||||
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_RELEASE_MATTERMOST_WEBHOOK_URL }}"
|
||||
FASTLANE_TEAM_ID: "${{ secrets.MM_MOBILE_FASTLANE_TEAM_ID }}"
|
||||
IOS_API_ISSUER_ID: "${{ secrets.MM_MOBILE_IOS_API_ISSUER_ID }}"
|
||||
IOS_API_KEY: "${{ secrets.MM_MOBILE_IOS_API_KEY }}"
|
||||
IOS_API_KEY_ID: "${{ secrets.MM_MOBILE_IOS_API_KEY_ID }}"
|
||||
MATCH_GIT_URL: "${{ secrets.MM_MOBILE_MATCH_GIT_URL }}"
|
||||
MATCH_PASSWORD: "${{ secrets.MM_MOBILE_MATCH_PASSWORD }}"
|
||||
SENTRY_AUTH_TOKEN: "${{ secrets.MM_MOBILE_SENTRY_AUTH_TOKEN }}"
|
||||
SENTRY_DSN_IOS: ${{ secrets.MM_MOBILE_RELEASE_SENTRY_DSN_IOS }}
|
||||
run: |
|
||||
echo "::group::Build"
|
||||
bundle exec fastlane ios build --env ios.release
|
||||
echo "::endgroup::"
|
||||
echo "::group::Deploy to TestFlight"
|
||||
bundle exec fastlane ios deploy file:"${{ github.workspace }}/*.ipa" --env ios.release
|
||||
echo "::endgroup::"
|
||||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-ios-release-build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ios-build-release-${{ github.run_id }}
|
||||
path: "*.ipa"
|
||||
|
||||
build-ios-simulator:
|
||||
runs-on: macos-14-large
|
||||
if: ${{ !contains(github.ref_name , 'release-ios') }}
|
||||
needs:
|
||||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
||||
- name: ci/build-ios-simulator
|
||||
env:
|
||||
TAG: "${{ github.ref_name }}"
|
||||
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
|
||||
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_BETA_MATTERMOST_WEBHOOK_URL }}"
|
||||
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
|
||||
run: bundle exec fastlane ios simulator --env ios.simulator
|
||||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-ios-pr-simulator
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ios-build-simulator-${{ github.run_id }}
|
||||
path: Mattermost-simulator-x86_64.app.zip
|
||||
100
.github/workflows/build-pr.yml
vendored
100
.github/workflows/build-pr.yml
vendored
@@ -1,100 +0,0 @@
|
||||
---
|
||||
name: build-pr
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.14.0
|
||||
TERM: xterm
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.event.label.name == 'Build Apps for PR' || github.event.label.name == 'Build App for iOS' || github.event.label.name == 'Build App for Android' }}
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
||||
build-ios-pr:
|
||||
runs-on: macos-14-large
|
||||
if: ${{ github.event.label.name == 'Build Apps for PR' || github.event.label.name == 'Build App for iOS' }}
|
||||
needs:
|
||||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
||||
- name: ci/output-ssh-private-key
|
||||
shell: bash
|
||||
run: |
|
||||
SSH_KEY_PATH=~/.ssh/id_ed25519
|
||||
mkdir -p ~/.ssh
|
||||
echo -e '${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}' > ${SSH_KEY_PATH}
|
||||
chmod 0600 ${SSH_KEY_PATH}
|
||||
ssh-keygen -y -f ${SSH_KEY_PATH} > ${SSH_KEY_PATH}.pub
|
||||
|
||||
- name: ci/build-ios-pr
|
||||
env:
|
||||
BRANCH_TO_BUILD: "${{ github.event.pull_request.head.ref }}"
|
||||
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_PR_AWS_ACCESS_KEY_ID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_PR_AWS_SECRET_ACCESS_KEY }}"
|
||||
FASTLANE_TEAM_ID: "${{ secrets.MM_MOBILE_FASTLANE_TEAM_ID }}"
|
||||
IOS_API_ISSUER_ID: "${{ secrets.MM_MOBILE_IOS_API_ISSUER_ID }}"
|
||||
IOS_API_KEY: "${{ secrets.MM_MOBILE_IOS_API_KEY }}"
|
||||
IOS_API_KEY_ID: "${{ secrets.MM_MOBILE_IOS_API_KEY_ID }}"
|
||||
MATCH_GIT_URL: "${{ secrets.MM_MOBILE_MATCH_GIT_URL }}"
|
||||
MATCH_PASSWORD: "${{ secrets.MM_MOBILE_MATCH_PASSWORD }}"
|
||||
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_PR_MATTERMOST_WEBHOOK_URL }}"
|
||||
run: bundle exec fastlane ios build --env ios.pr
|
||||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-ios-pr-build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ios-build-pr-${{ github.run_id }}
|
||||
path: "*.ipa"
|
||||
|
||||
build-android-pr:
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.event.label.name == 'Build Apps for PR' || github.event.label.name == 'Build App for Android' }}
|
||||
needs:
|
||||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: ci/prepare-android-build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
env:
|
||||
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
|
||||
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
|
||||
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
|
||||
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
|
||||
|
||||
- name: ci/build-android-pr
|
||||
env:
|
||||
BRANCH_TO_BUILD: "${{ github.event.pull_request.head.ref }}"
|
||||
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_PR_AWS_ACCESS_KEY_ID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_PR_AWS_SECRET_ACCESS_KEY }}"
|
||||
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_PR_MATTERMOST_WEBHOOK_URL }}"
|
||||
run: bundle exec fastlane android build --env android.pr
|
||||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-android-pr-build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: android-build-pr-${{ github.run_id }}
|
||||
path: "*.apk"
|
||||
29
.github/workflows/ci.yml
vendored
29
.github/workflows/ci.yml
vendored
@@ -1,29 +0,0 @@
|
||||
---
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'release*'
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.14.0
|
||||
TERM: xterm
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
- name: ci/test-coverage
|
||||
if: github.event_name == 'pull_request' || github.ref_name == 'main'
|
||||
uses: ./.github/actions/test-coverage
|
||||
with:
|
||||
run_id: ${{ github.run_id }}
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -26,17 +26,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
|
||||
# Autobuild attempts to build any compiled languages
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
||||
129
.github/workflows/compatibility-matrix-testing.yml
vendored
129
.github/workflows/compatibility-matrix-testing.yml
vendored
@@ -1,129 +0,0 @@
|
||||
name: Compatibility Matrix Testing
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
CMT_MATRIX:
|
||||
description: "A JSON object representing the testing matrix"
|
||||
required: true
|
||||
type: string
|
||||
MOBILE_VERSION:
|
||||
description: "The mattermost mobile version to test"
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
## This is picked up after the finish for cleanup
|
||||
upload-cmt-server-detals:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: cmt/generate-instance-details-file
|
||||
run: echo '${{ inputs.CMT_MATRIX }}' > instance-details.json
|
||||
|
||||
- name: cmt/upload-instance-details
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: instance-details.json
|
||||
path: instance-details.json
|
||||
retention-days: 1
|
||||
|
||||
calculate-commit-hash:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
MOBILE_SHA: ${{ steps.repo.outputs.MOBILE_SHA }}
|
||||
steps:
|
||||
- name: cmt/checkout-mobile
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
- name: cmt/calculate-mattermost-sha
|
||||
id: repo
|
||||
run: echo "MOBILE_SHA=$(git rev-parse HEAD)" >> ${GITHUB_OUTPUT}
|
||||
|
||||
update-initial-status:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- calculate-commit-hash
|
||||
steps:
|
||||
- uses: mattermost/actions/delivery/update-commit-status@d5174b860704729f4c14ef8489ae075742bfa08a
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
repository_full_name: ${{ github.repository }}
|
||||
commit_sha: ${{ needs.calculate-commit-hash.outputs.MOBILE_SHA }}
|
||||
context: e2e/compatibility-matrix-testing
|
||||
description: "Compatibility Matrix Testing for ${{ inputs.MOBILE_VERSION }} version"
|
||||
status: pending
|
||||
|
||||
# Input follows the below schema
|
||||
# {
|
||||
# "server": [
|
||||
# {
|
||||
# "version": "9.6.1",
|
||||
# "url": "https://delivery-cmt-8467830017-9-6-1.test.mattermost.cloud/"
|
||||
# },
|
||||
# {
|
||||
# "version": "9.5.2",
|
||||
# "url": "https://delivery-cmt-8467830017-9-5-2.test.mattermost.cloud/"
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
build-ios-simulator:
|
||||
runs-on: macos-14
|
||||
needs:
|
||||
- update-initial-status
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
- name: Prepare iOS Build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
||||
- name: Build iOS Simulator
|
||||
env:
|
||||
TAG: "${{ inputs.MOBILE_VERSION }}"
|
||||
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
|
||||
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
|
||||
run: bundle exec fastlane ios simulator --env ios.simulator
|
||||
working-directory: ./fastlane
|
||||
|
||||
- name: Upload iOS Simulator Build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ios-build-simulator-${{ github.run_id }}
|
||||
path: Mattermost-simulator-x86_64.app.zip
|
||||
|
||||
detox-e2e:
|
||||
name: mobile-cmt-${{ matrix.server.version }}
|
||||
uses: ./.github/workflows/e2e-detox-template.yml
|
||||
needs:
|
||||
- build-ios-simulator
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(inputs.CMT_MATRIX) }}
|
||||
secrets: inherit
|
||||
with:
|
||||
run-ios-tests: true
|
||||
run-type: "RELEASE"
|
||||
MM_TEST_SERVER_URL: ${{ matrix.server.url }}
|
||||
MOBILE_VERSION: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
update-final-status:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- calculate-commit-hash
|
||||
- detox-e2e
|
||||
steps:
|
||||
- uses: mattermost/actions/delivery/update-commit-status@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
repository_full_name: ${{ github.repository }}
|
||||
commit_sha: ${{ needs.calculate-commit-hash.outputs.MOBILE_SHA }}
|
||||
context: e2e/compatibility-matrix-testing
|
||||
description: Compatibility Matrix Testing for ${{ inputs.MOBILE_VERSION }} version
|
||||
status: ${{ needs.detox-e2e.outputs.STATUS }}
|
||||
target_url: ${{ needs.detox-e2e.outputs.TARGET_URL }}
|
||||
345
.github/workflows/e2e-android-template.yml
vendored
345
.github/workflows/e2e-android-template.yml
vendored
@@ -1,345 +0,0 @@
|
||||
name: Detox Android E2E Tests Template
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
MM_TEST_SERVER_URL:
|
||||
description: "The test server URL"
|
||||
required: false
|
||||
type: string
|
||||
MM_TEST_USER_NAME:
|
||||
description: "The admin username of the test instance"
|
||||
required: false
|
||||
type: string
|
||||
MM_TEST_PASSWORD:
|
||||
description: "The admin password of the test instance"
|
||||
required: false
|
||||
type: string
|
||||
MOBILE_VERSION:
|
||||
description: "The mobile version to test"
|
||||
required: false
|
||||
default: ${{ github.head_ref || github.ref }}
|
||||
type: string
|
||||
run-android-tests:
|
||||
description: "Run Android tests"
|
||||
required: true
|
||||
type: boolean
|
||||
run-type:
|
||||
type: string
|
||||
required: false
|
||||
default: "PR"
|
||||
testcase_failure_fatal:
|
||||
description: "Should failures be considered fatal"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
record_tests_in_zephyr:
|
||||
description: "Record test results in Zephyr, typically for nightly and release runs"
|
||||
required: false
|
||||
type: string
|
||||
default: 'false'
|
||||
low_bandwidth_mode:
|
||||
description: "Enable low bandwidth mode"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
android_avd_name:
|
||||
description: "Android Emulator name"
|
||||
required: false
|
||||
type: string
|
||||
default: "detox_pixel_4_xl"
|
||||
android_api_level:
|
||||
description: "Android API level"
|
||||
required: false
|
||||
type: string
|
||||
default: "34"
|
||||
outputs:
|
||||
STATUS:
|
||||
value: ${{ jobs.generate-report.outputs.STATUS }}
|
||||
TARGET_URL:
|
||||
value: ${{ jobs.generate-report.outputs.TARGET_URL }}
|
||||
FAILURES:
|
||||
value: ${{ jobs.generate-report.outputs.FAILURES }}
|
||||
|
||||
env:
|
||||
AWS_REGION: "us-east-1"
|
||||
ADMIN_EMAIL: ${{ secrets.MM_MOBILE_E2E_ADMIN_EMAIL }}
|
||||
ADMIN_USERNAME: ${{ secrets.MM_MOBILE_E2E_ADMIN_USERNAME }}
|
||||
ADMIN_PASSWORD: ${{ secrets.MM_MOBILE_E2E_ADMIN_PASSWORD }}
|
||||
BRANCH: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}
|
||||
COMMIT_HASH: ${{ github.sha }}
|
||||
DEVICE_NAME: ${{ inputs.android_avd_name }} # This is needed to split tests as same code is used in iOS job
|
||||
DEVICE_OS_VERSION: ${{ inputs.android_api_level }}
|
||||
DETOX_AWS_S3_BUCKET: "mattermost-detox-report"
|
||||
HEADLESS: "true"
|
||||
TYPE: ${{ inputs.run-type }}
|
||||
PULL_REQUEST: "https://github.com/mattermost/mattermost-mobile/pull/${{ github.event.number }}"
|
||||
SITE_1_URL: ${{ inputs.MM_TEST_SERVER_URL || 'https://mobile-e2e-site-1.test.mattermost.cloud' }}
|
||||
SITE_2_URL: "https://mobile-e2e-site-2.test.mattermost.cloud"
|
||||
SITE_3_URL: "https://mobile-e2e-site-3.test.mattermost.cloud"
|
||||
ZEPHYR_ENABLE: ${{ inputs.record_tests_in_zephyr }}
|
||||
JIRA_PROJECT_KEY: "MM"
|
||||
ZEPHYR_API_KEY: ${{ secrets.MM_MOBILE_E2E_ZEPHYR_API_KEY }}
|
||||
ZEPHYR_FOLDER_ID: "3233873"
|
||||
TEST_CYCLE_LINK_PREFIX: ${{ secrets.MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX }}
|
||||
WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }}
|
||||
FAILURE_MESSAGE: "Something has failed"
|
||||
IOS: "false"
|
||||
RUNNING_E2E: "true"
|
||||
AVD_NAME: ${{ inputs.android_avd_name }}
|
||||
SDK_VERSION: ${{ inputs.android_api_level }}
|
||||
|
||||
jobs:
|
||||
generate-specs:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
specs: ${{ steps.generate-specs.outputs.specs }}
|
||||
build_id: ${{ steps.resolve-device.outputs.BUILD_ID }}
|
||||
mobile_sha: ${{ steps.resolve-device.outputs.MOBILE_SHA }}
|
||||
mobile_ref: ${{ steps.resolve-device.outputs.MOBILE_REF }}
|
||||
workflow_hash: ${{ steps.resolve-device.outputs.WORKFLOW_HASH }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
- name: Set Build ID
|
||||
id: resolve-device
|
||||
run: |
|
||||
BUILD_ID="${{ github.run_id }}-${{ env.AVD_NAME }}-${{ env.SDK_VERSION}}"
|
||||
WORKFLOW_HASH=$(tr -dc a-z0-9 </dev/urandom | head -c 10)
|
||||
|
||||
## We need that hash to separate the artifacts
|
||||
echo "WORKFLOW_HASH=${WORKFLOW_HASH}" >> ${GITHUB_OUTPUT}
|
||||
|
||||
echo "BUILD_ID=$(echo ${BUILD_ID} | sed 's/ /_/g')" >> ${GITHUB_OUTPUT}
|
||||
echo "MOBILE_SHA=$(git rev-parse HEAD)" >> ${GITHUB_OUTPUT}
|
||||
echo "MOBILE_REF=$(git rev-parse --abbrev-ref HEAD)" >> ${GITHUB_OUTPUT}
|
||||
|
||||
- name: Generate Test Specs
|
||||
id: generate-specs
|
||||
uses: ./.github/actions/generate-specs
|
||||
with:
|
||||
parallelism: 10
|
||||
search_path: detox/e2e/test
|
||||
device_name: ${{ env.AVD_NAME }}
|
||||
device_os_version: ${{ env.SDK_VERSION }}
|
||||
|
||||
e2e-android:
|
||||
name: android-detox-e2e-${{ matrix.runId }}-${{ matrix.deviceName }}-${{ matrix.deviceOsVersion }}
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
continue-on-error: true
|
||||
timeout-minutes: 240
|
||||
env:
|
||||
ANDROID_HOME: /usr/local/lib/android/sdk
|
||||
ANDROID_SDK_ROOT: /usr/local/lib/android/sdk
|
||||
needs:
|
||||
- generate-specs
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.generate-specs.outputs.specs) }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libpulse0
|
||||
sudo apt-get install -y scrot ffmpeg xvfb
|
||||
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: Prepare Android Build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
env:
|
||||
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
|
||||
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
|
||||
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
|
||||
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
|
||||
|
||||
- name: Install Detox Dependencies
|
||||
run: |
|
||||
cd detox
|
||||
npm install
|
||||
|
||||
- name: Set .env with RUNNING_E2E=true
|
||||
run: |
|
||||
cat > .env <<EOF
|
||||
RUNNING_E2E=true
|
||||
EOF
|
||||
|
||||
- name: Create destination path
|
||||
run: mkdir -p android/app/build
|
||||
|
||||
- name: Download APK artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: android-build-files-${{ github.run_id }}
|
||||
path: android/app/build
|
||||
|
||||
- name: Set up Android SDK
|
||||
run: |
|
||||
export ANDROID_HOME=/usr/local/lib/android/sdk
|
||||
export PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/emulator:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools:$PATH
|
||||
echo "ANDROID_HOME=$ANDROID_HOME" >> $GITHUB_ENV
|
||||
echo "PATH=$PATH" >> $GITHUB_ENV
|
||||
|
||||
- name: Start Xvfb
|
||||
run: |
|
||||
Xvfb :99 -screen 0 1920x1080x24 &
|
||||
export DISPLAY=:99
|
||||
echo "DISPLAY=:99" >> $GITHUB_ENV
|
||||
|
||||
- name: Accept Android licenses
|
||||
run: |
|
||||
mkdir -p ~/.android
|
||||
yes | sdkmanager --licenses > sdk_licenses_output.txt || true
|
||||
|
||||
# Check if "All SDK package licenses accepted" appears in output
|
||||
if grep -q "All SDK package licenses accepted" sdk_licenses_output.txt; then
|
||||
echo "✅ All licenses accepted successfully."
|
||||
else
|
||||
echo "❌ Licenses not fully accepted."
|
||||
cat sdk_licenses_output.txt
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install Android SDK components
|
||||
run: |
|
||||
yes | sdkmanager --install "platform-tools" "emulator" "platforms;android-34" "system-images;android-34;default;x86_64" "system-images;android-34;google_apis;x86_64"
|
||||
env:
|
||||
JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }}
|
||||
|
||||
- name: Create and run Android Emulator
|
||||
run: |
|
||||
cd detox
|
||||
chmod +x ./create_android_emulator.sh
|
||||
CI=true ./create_android_emulator.sh ${{ env.SDK_VERSION }} ${{ env.AVD_NAME }} ${{ matrix.specs }}
|
||||
continue-on-error: true # We want to run all the tests
|
||||
|
||||
- name: Upload Android Test Report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: android-results-${{ needs.generate-specs.outputs.workflow_hash }}-${{ matrix.runId }}
|
||||
path: detox/artifacts/
|
||||
|
||||
generate-report:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- generate-specs
|
||||
- e2e-android
|
||||
outputs:
|
||||
TARGET_URL: ${{ steps.set-url.outputs.TARGET_URL }}
|
||||
STATUS: ${{ steps.determine-status.outputs.STATUS }}
|
||||
FAILURES: ${{ steps.summary.outputs.FAILURES }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
- name: ci/prepare-node-deps
|
||||
uses: ./.github/actions/prepare-node-deps
|
||||
|
||||
- name: Download Android Artifacts
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
with:
|
||||
path: detox/artifacts/
|
||||
pattern: android-results-${{ needs.generate-specs.outputs.workflow_hash }}-*
|
||||
continue-on-error: true
|
||||
|
||||
- name: Generate Report Path
|
||||
id: s3
|
||||
run: |
|
||||
path="${{ needs.generate-specs.outputs.build_id }}-${{ needs.generate-specs.outputs.mobile_sha }}-${{ needs.generate-specs.outputs.mobile_ref }}"
|
||||
echo "path=$(echo "${path}" | sed 's/\./-/g')" >> ${GITHUB_OUTPUT}
|
||||
|
||||
- name: Save report Detox Dependencies
|
||||
id: report-link
|
||||
run: |
|
||||
cd detox
|
||||
npm ci
|
||||
npm run e2e:save-report
|
||||
env:
|
||||
DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }}
|
||||
DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }}
|
||||
BUILD_ID: ${{ needs.generate-specs.outputs.build_id }}
|
||||
REPORT_PATH: ${{ steps.s3.outputs.path }}
|
||||
## These are needed for the MM Webhook report
|
||||
COMMIT_HASH: ${{ needs.generate-specs.outputs.mobile_sha }}
|
||||
BRANCH: ${{ needs.generate-specs.outputs.mobile_ref }}
|
||||
|
||||
- name: Calculate failures
|
||||
id: summary
|
||||
run: |
|
||||
echo "FAILURES=$(cat detox/artifacts/summary.json | jq .stats.failures)" >> ${GITHUB_OUTPUT}
|
||||
echo "PASSES=$(cat detox/artifacts/summary.json | jq .stats.passes)" >> ${GITHUB_OUTPUT}
|
||||
echo "SKIPPED=$(cat detox/artifacts/summary.json | jq .stats.skipped)" >> ${GITHUB_OUTPUT}
|
||||
echo "TOTAL=$(cat detox/artifacts/summary.json | jq .stats.tests)" >> ${GITHUB_OUTPUT}
|
||||
echo "ERRORS=$(cat detox/artifacts/summary.json | jq .stats.errors)" >> ${GITHUB_OUTPUT}
|
||||
echo "PERCENTAGE=$(cat detox/artifacts/summary.json | jq .stats.passPercent)" >> ${GITHUB_OUTPUT}
|
||||
|
||||
- name: Set Target URL
|
||||
id: set-url
|
||||
run: |
|
||||
echo "TARGET_URL=https://${{ env.DETOX_AWS_S3_BUCKET }}.s3.amazonaws.com/${{ steps.s3.outputs.path }}/jest-stare/android-report.html" >> ${GITHUB_OUTPUT}
|
||||
|
||||
- name: Determine Status
|
||||
id: determine-status
|
||||
run: |
|
||||
if [[ ${{ steps.summary.outputs.failures }} -gt 0 && "${{ inputs.testcase_failure_fatal }}" == "true" ]]; then
|
||||
echo "STATUS=failure" >> ${GITHUB_OUTPUT}
|
||||
else
|
||||
echo "STATUS=success" >> ${GITHUB_OUTPUT}
|
||||
fi
|
||||
|
||||
- name: Generate Summary
|
||||
run: |
|
||||
echo "| Tests | Passed :white_check_mark: | Failed :x: | Skipped :fast_forward: | Errors :warning: | " >> ${GITHUB_STEP_SUMMARY}
|
||||
echo "|:---:|:---:|:---:|:---:|:---:|" >> ${GITHUB_STEP_SUMMARY}
|
||||
echo "| ${{ steps.summary.outputs.TOTAL }} | ${{ steps.summary.outputs.PASSES }} | ${{ steps.summary.outputs.FAILURES }} | ${{ steps.summary.outputs.SKIPPED }} | ${{ steps.summary.outputs.ERRORS }} |" >> ${GITHUB_STEP_SUMMARY}
|
||||
echo "" >> ${GITHUB_STEP_SUMMARY}
|
||||
echo "You can check the full report [here](${{ steps.set-url.outputs.TARGET_URL }})" >> ${GITHUB_STEP_SUMMARY}
|
||||
echo "There was **${{ steps.summary.outputs.PERCENTAGE }}%** success rate." >> ${GITHUB_STEP_SUMMARY}
|
||||
|
||||
- name: Comment report on the PR
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
|
||||
const commentBody = `**Android E2E Test Report**: ${process.env.MOBILE_SHA} | ${process.env.PERCENTAGE}% (${process.env.PASSES}/${process.env.TOTAL}) | [full report](${process.env.TARGET_URL})
|
||||
| Tests | Passed ✅ | Failed ❌ | Skipped ⏭️ | Errors ⚠️ |
|
||||
|:---:|:---:|:---:|:---:|:---:|
|
||||
| ${process.env.TOTAL} | ${process.env.PASSES} | ${process.env.FAILURES} | ${process.env.SKIPPED} | ${process.env.ERRORS} |
|
||||
`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body: commentBody,
|
||||
});
|
||||
env:
|
||||
STATUS: ${{ steps.determine-status.outputs.STATUS }}
|
||||
FAILURES: ${{ steps.summary.outputs.FAILURES }}
|
||||
PASSES: ${{ steps.summary.outputs.PASSES }}
|
||||
SKIPPED: ${{ steps.summary.outputs.SKIPPED }}
|
||||
TOTAL: ${{ steps.summary.outputs.TOTAL }}
|
||||
ERRORS: ${{ steps.summary.outputs.ERRORS }}
|
||||
PERCENTAGE: ${{ steps.summary.outputs.PERCENTAGE }}
|
||||
BUILD_ID: ${{ needs.generate-specs.outputs.build_id }}
|
||||
RUN_TYPE: ${{ inputs.run-type }}
|
||||
MOBILE_REF: ${{ needs.generate-specs.outputs.mobile_ref }}
|
||||
MOBILE_SHA: ${{ needs.generate-specs.outputs.mobile_sha }}
|
||||
TARGET_URL: ${{ steps.set-url.outputs.TARGET_URL }}
|
||||
217
.github/workflows/e2e-detox-pr.yml
vendored
217
.github/workflows/e2e-detox-pr.yml
vendored
@@ -1,217 +0,0 @@
|
||||
# Can be used to run Detox E2E tests on pull requests for the Mattermost mobile app with low bandwidth
|
||||
# by using 'E2E iOS tests for PR (LBW 1)' instead.
|
||||
name: Detox E2E Tests PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- feature_schedule_posts
|
||||
types:
|
||||
- labeled
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.event.pull_request.number }}-${{ github.event.label.name }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
update-initial-status-ios:
|
||||
if: contains(github.event.label.name, 'E2E iOS tests for PR')
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: mattermost/actions/delivery/update-commit-status@main
|
||||
if: contains(github.event.label.name, 'E2E iOS tests for PR')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
repository_full_name: ${{ github.repository }}
|
||||
commit_sha: ${{ github.event.pull_request.head.sha }}
|
||||
context: e2e/detox-ios-tests
|
||||
description: Detox iOS tests for mattermost mobile app have started ...
|
||||
status: pending
|
||||
|
||||
update-initial-status-android:
|
||||
runs-on: ubuntu-22.04
|
||||
if: contains(github.event.label.name, 'E2E Android tests for PR')
|
||||
steps:
|
||||
- uses: mattermost/actions/delivery/update-commit-status@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
repository_full_name: ${{ github.repository }}
|
||||
commit_sha: ${{ github.event.pull_request.head.sha }}
|
||||
context: e2e/detox-android-tests
|
||||
description: Detox Android tests for mattermost mobile app have started ...
|
||||
status: pending
|
||||
|
||||
build-ios-simulator:
|
||||
if: contains(github.event.label.name, 'E2E iOS tests for PR')
|
||||
runs-on: macos-14
|
||||
needs:
|
||||
- update-initial-status-ios
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Prepare iOS Build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
||||
- name: Build iOS Simulator
|
||||
env:
|
||||
TAG: "${{ github.event.pull_request.head.sha }}"
|
||||
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
|
||||
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
|
||||
run: bundle exec fastlane ios simulator --env ios.simulator
|
||||
working-directory: ./fastlane
|
||||
|
||||
- name: Upload iOS Simulator Build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ios-build-simulator-${{ github.run_id }}
|
||||
path: Mattermost-simulator-x86_64.app.zip
|
||||
|
||||
build-android-apk:
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
if: contains(github.event.label.name, 'E2E Android tests for PR')
|
||||
needs:
|
||||
- update-initial-status-android
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_jvmargs: -Xmx8g
|
||||
steps:
|
||||
- name: Prune Docker to free up space
|
||||
run: docker system prune -af
|
||||
|
||||
- name: Remove npm Temporary Files
|
||||
run: |
|
||||
rm -rf ~/.npm/_cacache
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Prepare Android Build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
env:
|
||||
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
|
||||
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
|
||||
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
|
||||
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: sudo apt-get clean && sudo apt-get update && sudo apt-get install -y default-jdk
|
||||
|
||||
- name: Detox build
|
||||
run: |
|
||||
cd detox
|
||||
npm install
|
||||
npm install -g detox-cli
|
||||
npm run e2e:android-inject-settings
|
||||
npm run e2e:android-build
|
||||
|
||||
- name: Upload Android Build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: android-build-files-${{ github.run_id }}
|
||||
path: "android/app/build/**/*"
|
||||
|
||||
run-ios-tests-on-pr:
|
||||
if: contains(github.event.label.name, 'E2E iOS tests for PR')
|
||||
name: iOS Mobile Tests on PR
|
||||
uses: ./.github/workflows/e2e-ios-template.yml
|
||||
needs:
|
||||
- build-ios-simulator
|
||||
with:
|
||||
run-type: "PR"
|
||||
MOBILE_VERSION: ${{ github.event.pull_request.head.sha }}
|
||||
low_bandwidth_mode: ${{ contains(github.event.label.name,'LBW') && true || false }}
|
||||
secrets: inherit
|
||||
|
||||
run-android-tests-on-pr:
|
||||
if: contains(github.event.label.name, 'E2E Android tests for PR')
|
||||
name: Android Mobile Tests on PR
|
||||
uses: ./.github/workflows/e2e-android-template.yml
|
||||
needs:
|
||||
- build-android-apk
|
||||
with:
|
||||
run-android-tests: true
|
||||
run-type: "PR"
|
||||
MOBILE_VERSION: ${{ github.event.pull_request.head.sha }}
|
||||
secrets: inherit
|
||||
|
||||
update-final-status-ios:
|
||||
runs-on: ubuntu-22.04
|
||||
if: contains(github.event.label.name, 'E2E iOS tests for PR')
|
||||
needs:
|
||||
- run-ios-tests-on-pr
|
||||
steps:
|
||||
- uses: mattermost/actions/delivery/update-commit-status@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
repository_full_name: ${{ github.repository }}
|
||||
commit_sha: ${{ github.event.pull_request.head.sha }}
|
||||
context: e2e/detox-ios-tests
|
||||
description: Completed with ${{ needs.run-ios-tests-on-pr.outputs.FAILURES }} failures
|
||||
status: ${{ needs.run-ios-tests-on-pr.outputs.STATUS }}
|
||||
target_url: ${{ needs.run-ios-tests-on-pr.outputs.TARGET_URL }}
|
||||
|
||||
update-final-status-android:
|
||||
runs-on: ubuntu-22.04
|
||||
if: contains(github.event.label.name, 'E2E Android tests for PR')
|
||||
needs:
|
||||
- run-android-tests-on-pr
|
||||
steps:
|
||||
- uses: mattermost/actions/delivery/update-commit-status@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
repository_full_name: ${{ github.repository }}
|
||||
commit_sha: ${{ github.event.pull_request.head.sha }}
|
||||
context: e2e/detox-android-tests
|
||||
description: Completed with ${{ needs.run-android-tests-on-pr.outputs.FAILURES }} failures
|
||||
status: ${{ needs.run-android-tests-on-pr.outputs.STATUS }}
|
||||
target_url: ${{ needs.run-android-tests-on-pr.outputs.TARGET_URL }}
|
||||
|
||||
e2e-remove-ios-label:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- run-ios-tests-on-pr
|
||||
steps:
|
||||
- name: e2e/remove-label-from-pr
|
||||
uses: actions/github-script@e7aeb8c663f696059ebb5f9ab1425ed2ef511bdb # v7.0.1
|
||||
continue-on-error: true # Label might have been removed manually
|
||||
with:
|
||||
script: |
|
||||
const iosLabel = 'E2E iOS tests for PR';
|
||||
context.payload.pull_request.labels.forEach(label => {
|
||||
if (label.name.includes(iosLabel)) {
|
||||
github.rest.issues.removeLabel({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: label.name,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
e2e-remove-android-label:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- run-android-tests-on-pr
|
||||
steps:
|
||||
- name: e2e/remove-label-from-pr
|
||||
uses: actions/github-script@e7aeb8c663f696059ebb5f9ab1425ed2ef511bdb # v7.0.1
|
||||
continue-on-error: true # Label might have been removed manually
|
||||
with:
|
||||
script: |
|
||||
const androidLabel = 'E2E Android tests for PR';
|
||||
context.payload.pull_request.labels.forEach(label => {
|
||||
if (label.name.includes(androidLabel)) {
|
||||
github.rest.issues.removeLabel({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: label.name,
|
||||
});
|
||||
}
|
||||
});
|
||||
164
.github/workflows/e2e-detox-release.yml
vendored
164
.github/workflows/e2e-detox-release.yml
vendored
@@ -1,164 +0,0 @@
|
||||
name: Detox E2E Tests Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release-*
|
||||
|
||||
jobs:
|
||||
update-initial-status-ios:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: mattermost/actions/delivery/update-commit-status@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
repository_full_name: ${{ github.repository }}
|
||||
commit_sha: ${{ github.sha }}
|
||||
context: e2e/detox-ios-tests
|
||||
description: Detox iOS tests for mattermost mobile app have started ...
|
||||
status: pending
|
||||
|
||||
update-initial-status-android:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: mattermost/actions/delivery/update-commit-status@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
repository_full_name: ${{ github.repository }}
|
||||
commit_sha: ${{ github.sha }}
|
||||
context: e2e/detox-android-tests
|
||||
description: Detox Android tests for mattermost mobile app have started ...
|
||||
status: pending
|
||||
|
||||
build-ios-simulator:
|
||||
runs-on: macos-14
|
||||
needs:
|
||||
- update-initial-status-ios
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Prepare iOS Build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
||||
- name: Build iOS Simulator
|
||||
env:
|
||||
TAG: "${{ github.ref }}"
|
||||
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
|
||||
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
|
||||
run: bundle exec fastlane ios simulator --env ios.simulator
|
||||
working-directory: ./fastlane
|
||||
|
||||
- name: Upload iOS Simulator Build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ios-build-simulator-${{ github.run_id }}
|
||||
path: Mattermost-simulator-x86_64.app.zip
|
||||
|
||||
build-android-apk:
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
needs:
|
||||
- update-initial-status-android
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_jvmargs: -Xmx8g
|
||||
steps:
|
||||
- name: Prune Docker to free up space
|
||||
run: docker system prune -af
|
||||
|
||||
- name: Remove npm Temporary Files
|
||||
run: |
|
||||
rm -rf ~/.npm/_cacache
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
- name: Prepare Android Build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
env:
|
||||
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
|
||||
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
|
||||
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
|
||||
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: sudo apt-get clean && sudo apt-get update && sudo apt-get install -y default-jdk
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.gradle/caches/modules-2/
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: ${{ runner.os }}-gradle-
|
||||
|
||||
- name: Detox build
|
||||
run: |
|
||||
cd detox
|
||||
npm install
|
||||
npm install -g detox-cli
|
||||
npm run e2e:android-build
|
||||
|
||||
- name: Upload Android Build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: android-build-files-${{ github.run_id }}
|
||||
path: "android/app/build/**/*"
|
||||
|
||||
run-ios-tests-on-release:
|
||||
name: iOS Mobile Tests on Release
|
||||
uses: ./.github/workflows/e2e-ios-template.yml
|
||||
needs:
|
||||
- build-ios-simulator
|
||||
with:
|
||||
run-type: "RELEASE"
|
||||
record_tests_in_zephyr: 'true'
|
||||
MOBILE_VERSION: ${{ github.ref }}
|
||||
secrets: inherit
|
||||
|
||||
run-android-tests-on-release:
|
||||
name: Android Mobile Tests on Release
|
||||
uses: ./.github/workflows/e2e-android-template.yml
|
||||
needs:
|
||||
- build-android-apk
|
||||
with:
|
||||
run-android-tests: true
|
||||
run-type: "RELEASE"
|
||||
record_tests_in_zephyr: 'true'
|
||||
MOBILE_VERSION: ${{ github.ref }}
|
||||
secrets: inherit
|
||||
|
||||
update-final-status-ios:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- run-ios-tests-on-release
|
||||
steps:
|
||||
- uses: mattermost/actions/delivery/update-commit-status@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
repository_full_name: ${{ github.repository }}
|
||||
commit_sha: ${{ github.sha }}
|
||||
context: e2e/detox-ios-tests
|
||||
description: Completed with ${{ needs.run-ios-tests-on-release.outputs.FAILURES }} failures
|
||||
status: ${{ needs.run-ios-tests-on-release.outputs.STATUS }}
|
||||
target_url: ${{ needs.run-ios-tests-on-release.outputs.TARGET_URL }}
|
||||
|
||||
update-final-status-android:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- run-android-tests-on-release
|
||||
steps:
|
||||
- uses: mattermost/actions/delivery/update-commit-status@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
repository_full_name: ${{ github.repository }}
|
||||
commit_sha: ${{ github.sha }}
|
||||
context: e2e/detox-android-tests
|
||||
description: Completed with ${{ needs.run-android-tests-on-release.outputs.FAILURES }} failures
|
||||
status: ${{ needs.run-android-tests-on-release.outputs.STATUS }}
|
||||
target_url: ${{ needs.run-android-tests-on-release.outputs.TARGET_URL }}
|
||||
163
.github/workflows/e2e-detox-scheduled.yml
vendored
163
.github/workflows/e2e-detox-scheduled.yml
vendored
@@ -1,163 +0,0 @@
|
||||
name: Detox E2E Tests (Scheduled)
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * 4,5" # Wednesday and Thursday midnight
|
||||
|
||||
jobs:
|
||||
update-initial-status-ios:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: mattermost/actions/delivery/update-commit-status@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
repository_full_name: ${{ github.repository }}
|
||||
commit_sha: ${{ github.sha }}
|
||||
context: e2e/detox-ios-tests
|
||||
description: Detox iOS tests for mattermost mobile app have started ...
|
||||
status: pending
|
||||
|
||||
update-initial-status-android:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: mattermost/actions/delivery/update-commit-status@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
repository_full_name: ${{ github.repository }}
|
||||
commit_sha: ${{ github.sha }}
|
||||
context: e2e/detox-android-tests
|
||||
description: Detox Android tests for mattermost mobile app have started ...
|
||||
status: pending
|
||||
|
||||
build-ios-simulator:
|
||||
runs-on: macos-14
|
||||
needs:
|
||||
- update-initial-status-ios
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Prepare iOS Build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
||||
- name: Build iOS Simulator
|
||||
env:
|
||||
TAG: "${{ github.ref }}"
|
||||
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
|
||||
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
|
||||
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
|
||||
run: bundle exec fastlane ios simulator --env ios.simulator
|
||||
working-directory: ./fastlane
|
||||
|
||||
- name: Upload iOS Simulator Build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ios-build-simulator-${{ github.run_id }}
|
||||
path: Mattermost-simulator-x86_64.app.zip
|
||||
|
||||
build-android-apk:
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
needs:
|
||||
- update-initial-status-android
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_jvmargs: -Xmx8g
|
||||
steps:
|
||||
- name: Prune Docker to free up space
|
||||
run: docker system prune -af
|
||||
|
||||
- name: Remove npm Temporary Files
|
||||
run: |
|
||||
rm -rf ~/.npm/_cacache
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
- name: Prepare Android Build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
env:
|
||||
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
|
||||
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
|
||||
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
|
||||
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: sudo apt-get clean && sudo apt-get update && sudo apt-get install -y default-jdk
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.gradle/caches/modules-2/
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: ${{ runner.os }}-gradle-
|
||||
|
||||
- name: Detox build
|
||||
run: |
|
||||
cd detox
|
||||
npm install
|
||||
npm install -g detox-cli
|
||||
npm run e2e:android-build
|
||||
|
||||
- name: Upload Android Build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: android-build-files-${{ github.run_id }}
|
||||
path: "android/app/build/**/*"
|
||||
|
||||
run-ios-tests-on-main-scheduled:
|
||||
name: iOS Mobile Tests on Main (Scheduled)
|
||||
uses: ./.github/workflows/e2e-ios-template.yml
|
||||
needs:
|
||||
- build-ios-simulator
|
||||
with:
|
||||
run-type: "MAIN"
|
||||
record_tests_in_zephyr: 'true'
|
||||
MOBILE_VERSION: ${{ github.ref }}
|
||||
secrets: inherit
|
||||
|
||||
run-android-tests-on-main-scheduled:
|
||||
name: Android Mobile Tests on Main (Scheduled)
|
||||
uses: ./.github/workflows/e2e-android-template.yml
|
||||
needs:
|
||||
- build-android-apk
|
||||
with:
|
||||
run-android-tests: true
|
||||
run-type: "MAIN"
|
||||
record_tests_in_zephyr: 'true'
|
||||
MOBILE_VERSION: ${{ github.ref }}
|
||||
secrets: inherit
|
||||
|
||||
update-final-status-ios:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- run-ios-tests-on-main-scheduled
|
||||
steps:
|
||||
- uses: mattermost/actions/delivery/update-commit-status@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
repository_full_name: ${{ github.repository }}
|
||||
commit_sha: ${{ github.sha }}
|
||||
context: e2e/detox-ios-tests
|
||||
description: Completed with ${{ needs.run-ios-tests-on-main-scheduled.outputs.FAILURES }} failures
|
||||
status: ${{ needs.run-ios-tests-on-main-scheduled.outputs.STATUS }}
|
||||
target_url: ${{ needs.run-ios-tests-on-main-scheduled.outputs.TARGET_URL }}
|
||||
|
||||
update-final-status-android:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- run-android-tests-on-main-scheduled
|
||||
steps:
|
||||
- uses: mattermost/actions/delivery/update-commit-status@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
repository_full_name: ${{ github.repository }}
|
||||
commit_sha: ${{ github.sha }}
|
||||
context: e2e/detox-android-tests
|
||||
description: Completed with ${{ needs.run-android-tests-on-main-scheduled.outputs.FAILURES }} failures
|
||||
status: ${{ needs.run-android-tests-on-main-scheduled.outputs.STATUS }}
|
||||
target_url: ${{ needs.run-android-tests-on-main-scheduled.outputs.TARGET_URL }}
|
||||
342
.github/workflows/e2e-ios-template.yml
vendored
342
.github/workflows/e2e-ios-template.yml
vendored
@@ -1,342 +0,0 @@
|
||||
name: Detox iOS E2E Tests Template
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
MM_TEST_SERVER_URL:
|
||||
description: "The test server URL"
|
||||
required: false
|
||||
type: string
|
||||
MM_TEST_USER_NAME:
|
||||
description: "The admin username of the test instance"
|
||||
required: false
|
||||
type: string
|
||||
MM_TEST_PASSWORD:
|
||||
description: "The admin password of the test instance"
|
||||
required: false
|
||||
type: string
|
||||
MOBILE_VERSION:
|
||||
description: "The mobile version to test"
|
||||
required: false
|
||||
default: ${{ github.head_ref || github.ref }}
|
||||
type: string
|
||||
run-type:
|
||||
type: string
|
||||
required: false
|
||||
default: "PR"
|
||||
testcase_failure_fatal:
|
||||
description: "Should failures be considered fatal"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
record_tests_in_zephyr:
|
||||
description: "Record test results in Zephyr, typically for nightly and release runs"
|
||||
required: false
|
||||
type: string
|
||||
default: 'false'
|
||||
ios_device_name:
|
||||
description: "iPhone simulator name"
|
||||
required: false
|
||||
type: string
|
||||
default: "iPhone 15 Pro"
|
||||
ios_device_os_name:
|
||||
description: "iPhone simulator OS version"
|
||||
required: false
|
||||
type: string
|
||||
default: "iOS 17.2"
|
||||
low_bandwidth_mode:
|
||||
description: "Enable low bandwidth mode"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
outputs:
|
||||
STATUS:
|
||||
value: ${{ jobs.generate-report.outputs.STATUS }}
|
||||
TARGET_URL:
|
||||
value: ${{ jobs.generate-report.outputs.TARGET_URL }}
|
||||
FAILURES:
|
||||
value: ${{ jobs.generate-report.outputs.FAILURES }}
|
||||
|
||||
env:
|
||||
AWS_REGION: "us-east-1"
|
||||
ADMIN_EMAIL: ${{ secrets.MM_MOBILE_E2E_ADMIN_EMAIL }}
|
||||
ADMIN_USERNAME: ${{ secrets.MM_MOBILE_E2E_ADMIN_USERNAME }}
|
||||
ADMIN_PASSWORD: ${{ secrets.MM_MOBILE_E2E_ADMIN_PASSWORD }}
|
||||
BRANCH: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}
|
||||
COMMIT_HASH: ${{ github.sha }}
|
||||
DEVICE_NAME: ${{ inputs.ios_device_name }}
|
||||
DEVICE_OS_VERSION: ${{ inputs.ios_device_os_name }}
|
||||
DETOX_AWS_S3_BUCKET: "mattermost-detox-report"
|
||||
HEADLESS: "true"
|
||||
TYPE: ${{ inputs.run-type }}
|
||||
PULL_REQUEST: "https://github.com/mattermost/mattermost-mobile/pull/${{ github.event.number }}"
|
||||
SITE_1_URL: ${{ inputs.MM_TEST_SERVER_URL || 'https://mobile-e2e-site-1.test.mattermost.cloud' }}
|
||||
SITE_2_URL: "https://mobile-e2e-site-2.test.mattermost.cloud"
|
||||
SITE_3_URL: "https://mobile-e2e-site-3.test.mattermost.cloud"
|
||||
ZEPHYR_ENABLE: ${{ inputs.record_tests_in_zephyr }}
|
||||
JIRA_PROJECT_KEY: "MM"
|
||||
ZEPHYR_API_KEY: ${{ secrets.MM_MOBILE_E2E_ZEPHYR_API_KEY }}
|
||||
ZEPHYR_FOLDER_ID: "3233873"
|
||||
TEST_CYCLE_LINK_PREFIX: ${{ secrets.MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX }}
|
||||
WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }}
|
||||
FAILURE_MESSAGE: "Something has failed"
|
||||
IOS: "true"
|
||||
RUNNING_E2E: "true"
|
||||
|
||||
jobs:
|
||||
generate-specs:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
specs: ${{ steps.generate-specs.outputs.specs }}
|
||||
build_id: ${{ steps.resolve-device.outputs.BUILD_ID }}
|
||||
mobile_sha: ${{ steps.resolve-device.outputs.MOBILE_SHA }}
|
||||
mobile_ref: ${{ steps.resolve-device.outputs.MOBILE_REF }}
|
||||
workflow_hash: ${{ steps.resolve-device.outputs.WORKFLOW_HASH }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
- name: Set Build ID
|
||||
id: resolve-device
|
||||
run: |
|
||||
BUILD_ID="${{ github.run_id }}-${{ env.DEVICE_NAME }}-${{ env.DEVICE_OS_VERSION}}"
|
||||
WORKFLOW_HASH=$(tr -dc a-z0-9 </dev/urandom | head -c 10)
|
||||
|
||||
## We need that hash to separate the artifacts
|
||||
echo "WORKFLOW_HASH=${WORKFLOW_HASH}" >> ${GITHUB_OUTPUT}
|
||||
|
||||
echo "BUILD_ID=$(echo ${BUILD_ID} | sed 's/ /_/g')" >> ${GITHUB_OUTPUT}
|
||||
echo "MOBILE_SHA=$(git rev-parse HEAD)" >> ${GITHUB_OUTPUT}
|
||||
echo "MOBILE_REF=$(git rev-parse --abbrev-ref HEAD)" >> ${GITHUB_OUTPUT}
|
||||
|
||||
- name: Generate Test Specs
|
||||
id: generate-specs
|
||||
uses: ./.github/actions/generate-specs
|
||||
with:
|
||||
parallelism: 10
|
||||
search_path: detox/e2e/test
|
||||
device_name: ${{ env.DEVICE_NAME }}
|
||||
device_os_version: ${{ env.DEVICE_OS_VERSION }}
|
||||
|
||||
e2e-ios:
|
||||
name: ios-detox-e2e-${{ matrix.runId }}-${{ matrix.deviceName }}-${{ matrix.deviceOsVersion }}
|
||||
runs-on: macos-14
|
||||
continue-on-error: true
|
||||
timeout-minutes: ${{ inputs.low_bandwidth_mode && 240 || 180 }}
|
||||
env:
|
||||
IOS: true
|
||||
needs:
|
||||
- generate-specs
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.generate-specs.outputs.specs) }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
- name: ci/prepare-node-deps
|
||||
uses: ./.github/actions/prepare-node-deps
|
||||
|
||||
- name: Install Homebrew Dependencies
|
||||
run: |
|
||||
brew tap wix/brew
|
||||
brew install applesimutils
|
||||
|
||||
- name: Download iOS Simulator Build
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
with:
|
||||
name: ios-build-simulator-${{ github.run_id }}
|
||||
path: mobile-artifacts
|
||||
|
||||
- name: Unzip iOS Simulator Build
|
||||
run: |
|
||||
unzip -o mobile-artifacts/*.zip -d mobile-artifacts/
|
||||
# delete zip file
|
||||
rm -f mobile-artifacts/*.zip
|
||||
|
||||
- name: Prepare Low Bandwidth Environment
|
||||
id: prepare-low-bandwidth
|
||||
uses: ./.github/actions/prepare-low-bandwidth
|
||||
if: ${{ inputs.low_bandwidth_mode }}
|
||||
with:
|
||||
test_server_url: ${{ env.SITE_1_URL }}
|
||||
device_name: ${{ env.DEVICE_NAME }}
|
||||
# all these value should be configurable
|
||||
download_speed: "3300"
|
||||
upload_speed: "3300"
|
||||
latency: "500"
|
||||
|
||||
- name: Install Detox Dependencies
|
||||
run: cd detox && npm i
|
||||
|
||||
- name: Start Proxy
|
||||
if: ${{ inputs.low_bandwidth_mode }}
|
||||
id: start-proxy
|
||||
uses: ./.github/actions/start-proxy
|
||||
with:
|
||||
test_server_url: ${{ env.SITE_1_URL }}
|
||||
|
||||
- name: Set .env with RUNNING_E2E=true
|
||||
run: |
|
||||
cat > .env <<EOF
|
||||
echo "RUNNING_E2E=true" >> .env
|
||||
|
||||
- name: Run Detox E2E Tests
|
||||
continue-on-error: true # We want to run all the tests
|
||||
run: |
|
||||
# Start the server
|
||||
npm run start &
|
||||
sleep 120 # Wait for watchman to finish querying the files
|
||||
cd detox
|
||||
npm run clean-detox
|
||||
npm run detox:config-gen
|
||||
npm run e2e:ios-test -- ${{ matrix.specs }}
|
||||
env:
|
||||
DETOX_DISABLE_HIERARCHY_DUMP: "YES"
|
||||
DETOX_DISABLE_SCREENSHOT_TRACKING: "YES"
|
||||
DETOX_LOGLEVEL: "debug"
|
||||
DETOX_DEVICE_TYPE: ${{ env.DEVICE_NAME }}
|
||||
DETOX_OS_VERSION: ${{ env.DEVICE_OS_VERSION }}
|
||||
LOW_BANDWIDTH_MODE: ${{ inputs.low_bandwidth_mode }}
|
||||
|
||||
- name: reset network settings
|
||||
if: ${{ inputs.low_bandwidth_mode || failure() }}
|
||||
run: |
|
||||
networksetup -setwebproxystate Ethernet "off"
|
||||
networksetup -setsecurewebproxystate Ethernet "off"
|
||||
|
||||
if (sudo pfctl -q -sa | grep 'Status: Enabled') then sudo pfctl -d; fi
|
||||
if (command -v pm2 &> /dev/null) then pm2 stop mitmdump; fi
|
||||
|
||||
sleep 5;
|
||||
|
||||
- name: Upload mitmdump Flow Output
|
||||
if: ${{ inputs.low_bandwidth_mode }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ios-mitmdump-flow-output-${{ needs.generate-specs.outputs.workflow_hash }}-${{ matrix.runId }}
|
||||
path: |
|
||||
/Users/runner/work/mattermost-mobile/mattermost-mobile/flow-output.csv
|
||||
/Users/runner/work/mattermost-mobile/mattermost-mobile/mitmdump.log
|
||||
|
||||
- name: Upload iOS Test Report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ios-results-${{ needs.generate-specs.outputs.workflow_hash }}-${{ matrix.runId }}
|
||||
path: detox/artifacts/
|
||||
|
||||
generate-report:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- generate-specs
|
||||
- e2e-ios
|
||||
outputs:
|
||||
TARGET_URL: ${{ steps.set-url.outputs.TARGET_URL }}
|
||||
STATUS: ${{ steps.determine-status.outputs.STATUS }}
|
||||
FAILURES: ${{ steps.summary.outputs.FAILURES }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
- name: ci/prepare-node-deps
|
||||
uses: ./.github/actions/prepare-node-deps
|
||||
|
||||
- name: Download iOS Artifacts
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
with:
|
||||
path: detox/artifacts/
|
||||
pattern: ios-results-${{ needs.generate-specs.outputs.workflow_hash }}-*
|
||||
|
||||
- name: Generate Report Path
|
||||
id: s3
|
||||
run: |
|
||||
path="${{ needs.generate-specs.outputs.build_id }}-${{ needs.generate-specs.outputs.mobile_sha }}-${{ needs.generate-specs.outputs.mobile_ref }}"
|
||||
echo "path=$(echo "${path}" | sed 's/\./-/g')" >> ${GITHUB_OUTPUT}
|
||||
|
||||
- name: Save report Detox Dependencies
|
||||
id: report-link
|
||||
run: |
|
||||
cd detox
|
||||
npm ci
|
||||
npm run e2e:save-report
|
||||
env:
|
||||
DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }}
|
||||
DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }}
|
||||
BUILD_ID: ${{ needs.generate-specs.outputs.build_id }}
|
||||
REPORT_PATH: ${{ steps.s3.outputs.path }}
|
||||
## These are needed for the MM Webhook report
|
||||
COMMIT_HASH: ${{ needs.generate-specs.outputs.mobile_sha }}
|
||||
BRANCH: ${{ needs.generate-specs.outputs.mobile_ref }}
|
||||
|
||||
- name: Calculate failures
|
||||
id: summary
|
||||
run: |
|
||||
echo "FAILURES=$(cat detox/artifacts/summary.json | jq .stats.failures)" >> ${GITHUB_OUTPUT}
|
||||
echo "PASSES=$(cat detox/artifacts/summary.json | jq .stats.passes)" >> ${GITHUB_OUTPUT}
|
||||
echo "SKIPPED=$(cat detox/artifacts/summary.json | jq .stats.skipped)" >> ${GITHUB_OUTPUT}
|
||||
echo "TOTAL=$(cat detox/artifacts/summary.json | jq .stats.tests)" >> ${GITHUB_OUTPUT}
|
||||
echo "ERRORS=$(cat detox/artifacts/summary.json | jq .stats.errors)" >> ${GITHUB_OUTPUT}
|
||||
echo "PERCENTAGE=$(cat detox/artifacts/summary.json | jq .stats.passPercent)" >> ${GITHUB_OUTPUT}
|
||||
|
||||
- name: Set Target URL
|
||||
id: set-url
|
||||
run: |
|
||||
echo "TARGET_URL=https://${{ env.DETOX_AWS_S3_BUCKET }}.s3.amazonaws.com/${{ steps.s3.outputs.path }}/jest-stare/ios-report.html" >> ${GITHUB_OUTPUT}
|
||||
|
||||
- name: Determine Status
|
||||
id: determine-status
|
||||
run: |
|
||||
if [[ ${{ steps.summary.outputs.failures }} -gt 0 && "${{ inputs.testcase_failure_fatal }}" == "true" ]]; then
|
||||
echo "STATUS=failure" >> ${GITHUB_OUTPUT}
|
||||
else
|
||||
echo "STATUS=success" >> ${GITHUB_OUTPUT}
|
||||
fi
|
||||
|
||||
- name: Generate Summary
|
||||
run: |
|
||||
echo "| Tests | Passed :white_check_mark: | Failed :x: | Skipped :fast_forward: | Errors :warning: | " >> ${GITHUB_STEP_SUMMARY}
|
||||
echo "|:---:|:---:|:---:|:---:|:---:|" >> ${GITHUB_STEP_SUMMARY}
|
||||
echo "| ${{ steps.summary.outputs.TOTAL }} | ${{ steps.summary.outputs.PASSES }} | ${{ steps.summary.outputs.FAILURES }} | ${{ steps.summary.outputs.SKIPPED }} | ${{ steps.summary.outputs.ERRORS }} |" >> ${GITHUB_STEP_SUMMARY}
|
||||
echo "" >> ${GITHUB_STEP_SUMMARY}
|
||||
echo "You can check the full report [here](${{ steps.set-url.outputs.TARGET_URL }})" >> ${GITHUB_STEP_SUMMARY}
|
||||
echo "There was **${{ steps.summary.outputs.PERCENTAGE }}%** success rate." >> ${GITHUB_STEP_SUMMARY}
|
||||
|
||||
- name: Comment report on the PR
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
|
||||
const commentBody = `**iOS E2E Test Report**: ${process.env.MOBILE_SHA} | ${process.env.PERCENTAGE}% (${process.env.PASSES}/${process.env.TOTAL}) | [full report](${process.env.TARGET_URL})
|
||||
| Tests | Passed ✅ | Failed ❌ | Skipped ⏭️ | Errors ⚠️ |
|
||||
|:---:|:---:|:---:|:---:|:---:|
|
||||
| ${process.env.TOTAL} | ${process.env.PASSES} | ${process.env.FAILURES} | ${process.env.SKIPPED} | ${process.env.ERRORS} |
|
||||
`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
body: commentBody,
|
||||
});
|
||||
env:
|
||||
STATUS: ${{ steps.determine-status.outputs.STATUS }}
|
||||
FAILURES: ${{ steps.summary.outputs.FAILURES }}
|
||||
PASSES: ${{ steps.summary.outputs.PASSES }}
|
||||
SKIPPED: ${{ steps.summary.outputs.SKIPPED }}
|
||||
TOTAL: ${{ steps.summary.outputs.TOTAL }}
|
||||
ERRORS: ${{ steps.summary.outputs.ERRORS }}
|
||||
PERCENTAGE: ${{ steps.summary.outputs.PERCENTAGE }}
|
||||
BUILD_ID: ${{ needs.generate-specs.outputs.build_id }}
|
||||
RUN_TYPE: ${{ inputs.run-type }}
|
||||
MOBILE_REF: ${{ needs.generate-specs.outputs.mobile_ref }}
|
||||
MOBILE_SHA: ${{ needs.generate-specs.outputs.mobile_sha }}
|
||||
TARGET_URL: ${{ steps.set-url.outputs.TARGET_URL }}
|
||||
234
.github/workflows/github-release.yml
vendored
234
.github/workflows/github-release.yml
vendored
@@ -1,234 +0,0 @@
|
||||
---
|
||||
name: github-release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v[0-9]+.[0-9]+.[0-9]+*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Release tag (e.g., v2.10.0)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
|
||||
SNYK_VERSION: "1.1297.2"
|
||||
CYCLONEDX_VERSION: "v0.27.2"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
||||
build-ios-unsigned:
|
||||
runs-on: macos-14-large
|
||||
needs:
|
||||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
||||
- name: ci/output-ssh-private-key
|
||||
shell: bash
|
||||
run: |
|
||||
SSH_KEY_PATH=~/.ssh/id_ed25519
|
||||
mkdir -p ~/.ssh
|
||||
echo -e '${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}' > ${SSH_KEY_PATH}
|
||||
chmod 0600 ${SSH_KEY_PATH}
|
||||
ssh-keygen -y -f ${SSH_KEY_PATH} > ${SSH_KEY_PATH}.pub
|
||||
|
||||
- name: ci/build-ios-unsigned
|
||||
env:
|
||||
TAG: "${{ env.RELEASE_TAG }}"
|
||||
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
|
||||
run: bundle exec fastlane ios unsigned
|
||||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-ios-unsigned
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
path: Mattermost-unsigned.ipa
|
||||
name: Mattermost-unsigned.ipa
|
||||
|
||||
- name: ci/install-snyk
|
||||
run: npm install -g snyk@${{ env.SNYK_VERSION }}
|
||||
|
||||
- name: ci/generate-ios-sbom
|
||||
env:
|
||||
SNYK_TOKEN: "${{ secrets.SNYK_TOKEN }}"
|
||||
run: |
|
||||
snyk sbom --format=cyclonedx1.6+json --json-file-output=../sbom-ios.json --all-projects
|
||||
working-directory: ./ios
|
||||
shell: bash
|
||||
|
||||
- name: ci/upload-ios-sbom
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
path: sbom-ios.json
|
||||
name: sbom-ios.json
|
||||
|
||||
build-android-unsigned:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/prepare-android-build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
with:
|
||||
sign: false
|
||||
|
||||
- name: ci/build-android-beta
|
||||
env:
|
||||
TAG: "${{ env.RELEASE_TAG }}"
|
||||
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
|
||||
run: bundle exec fastlane android unsigned
|
||||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-android-unsigned-build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
path: Mattermost-unsigned.apk
|
||||
name: Mattermost-unsigned.apk
|
||||
|
||||
- name: ci/install-snyk
|
||||
run: npm install -g snyk@${{ env.SNYK_VERSION }}
|
||||
|
||||
- name: ci/generate-android-sbom
|
||||
env:
|
||||
SNYK_TOKEN: "${{ secrets.SNYK_TOKEN }}"
|
||||
run: |
|
||||
snyk sbom --format=cyclonedx1.6+json --all-projects --json-file-output=../sbom-android.json
|
||||
working-directory: ./android
|
||||
shell: bash
|
||||
|
||||
- name: ci/upload-android-sbom
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
path: sbom-android.json
|
||||
name: sbom-android.json
|
||||
|
||||
generate-consolidated-sbom:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- build-ios-unsigned
|
||||
- build-android-unsigned
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/download-sboms
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
pattern: sbom-*.json
|
||||
path: ${{ github.workspace }}
|
||||
merge-multiple: true
|
||||
|
||||
- name: ci/install-snyk
|
||||
run: npm install -g snyk@${{ env.SNYK_VERSION }}
|
||||
|
||||
- name: ci/setup-cyclonedx-cli
|
||||
run: |
|
||||
set -e
|
||||
CYCLONEDX_BINARY="cyclonedx-linux-x64"
|
||||
CYCLONEDX_URL="https://github.com/CycloneDX/cyclonedx-cli/releases/download/${{ env.CYCLONEDX_VERSION }}/${CYCLONEDX_BINARY}"
|
||||
|
||||
# Download with better error handling and retry
|
||||
echo "Downloading CycloneDX CLI ${{ env.CYCLONEDX_VERSION }}..."
|
||||
curl -sSfL --retry 3 --retry-delay 5 "${CYCLONEDX_URL}" -o cyclonedx
|
||||
|
||||
# Verify the binary is executable and not corrupted
|
||||
if [ ! -s cyclonedx ]; then
|
||||
echo "Error: Downloaded file is empty or corrupted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make executable and move to PATH
|
||||
chmod +x cyclonedx
|
||||
sudo mv cyclonedx /usr/local/bin/
|
||||
|
||||
# Verify installation
|
||||
cyclonedx --version
|
||||
|
||||
- name: ci/generate-consolidated-sbom
|
||||
env:
|
||||
SNYK_TOKEN: "${{ secrets.SNYK_TOKEN }}"
|
||||
SBOM_FILENAME: "sbom-${{ github.event.repository.name }}-${{ env.RELEASE_TAG }}.json"
|
||||
run: |
|
||||
# Check if required SBOM files are available
|
||||
if [ ! -f "sbom-android.json" ]; then
|
||||
echo "Error: sbom-android.json not found. Android SBOM generation may have failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "sbom-ios.json" ]; then
|
||||
echo "Error: sbom-ios.json not found. iOS SBOM generation may have failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All required SBOM files are available. Proceeding with consolidation..."
|
||||
|
||||
# Generate top-level SBOM
|
||||
snyk sbom --format=cyclonedx1.6+json --json-file-output=sbom-top-level.json
|
||||
|
||||
# Consolidate SBOMs
|
||||
cyclonedx merge \
|
||||
--input-files "sbom-top-level.json" "sbom-android.json" "sbom-ios.json" \
|
||||
--input-format=json \
|
||||
--output-file="$SBOM_FILENAME" \
|
||||
--output-format=json \
|
||||
--output-version=v1_6
|
||||
|
||||
# Validate the consolidated SBOM
|
||||
cyclonedx validate --input-file="$SBOM_FILENAME"
|
||||
shell: bash
|
||||
|
||||
- name: ci/upload-consolidated-sbom
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
path: sbom-${{ github.event.repository.name }}-${{ env.RELEASE_TAG }}.json
|
||||
name: sbom-${{ github.event.repository.name }}-${{ env.RELEASE_TAG }}.json
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- build-ios-unsigned
|
||||
- build-android-unsigned
|
||||
- generate-consolidated-sbom
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- uses: ruby/setup-ruby@cb0fda56a307b8c78d38320cd40d9eb22a3bf04e # v1.242.0
|
||||
|
||||
- name: release/setup-fastlane-dependencies
|
||||
run: bundle install
|
||||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/download-artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
path: ${{ github.workspace }}
|
||||
merge-multiple: true
|
||||
|
||||
- name: release/create-github-release
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
|
||||
run: bundle exec fastlane github
|
||||
working-directory: ./fastlane
|
||||
|
||||
- name: release/upload-sbom-to-release
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
|
||||
run: |
|
||||
gh release upload "${{ env.RELEASE_TAG }}" "sbom-${{ github.event.repository.name }}-${{ env.RELEASE_TAG }}.json"
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -7,9 +7,6 @@ mattermost.keystore
|
||||
tmp/
|
||||
.env
|
||||
env.d.ts
|
||||
*.apk
|
||||
*.aab
|
||||
*.ipa
|
||||
|
||||
*/**/compass-icons.ttf
|
||||
|
||||
@@ -33,6 +30,8 @@ xcuserdata
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.apk
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
ios/Pods
|
||||
@@ -103,7 +102,6 @@ detox/detox_pixel_*
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
.bundle
|
||||
|
||||
#editor-settings
|
||||
.vscode
|
||||
@@ -115,10 +113,3 @@ launch.json
|
||||
|
||||
# Temporary files created by Metro to check the health of the file watcher
|
||||
.metro-health-check*
|
||||
|
||||
libraries/**/**/build
|
||||
libraries/**/**/.build
|
||||
|
||||
# Android sounds
|
||||
android/app/src/main/res/raw/*
|
||||
.aider*
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
sh ./scripts/pre-commit.sh
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
22.14.0
|
||||
@@ -1 +0,0 @@
|
||||
3.2.0
|
||||
22
.solidarity
22
.solidarity
@@ -4,11 +4,29 @@
|
||||
"output" : "moderate"
|
||||
},
|
||||
"requirements": {
|
||||
"Node": [
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "node",
|
||||
"semver": ">=16.0.0",
|
||||
"error": "install node using nvm https://github.com/nvm-sh/nvm#installing-and-updating"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "npm",
|
||||
"semver": ">=8.5.5 <9.0.0",
|
||||
"error": "install npm 8.5.5 `npm i -g npm@8.5.5"
|
||||
}
|
||||
],
|
||||
"Android": [
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "emulator"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "android"
|
||||
},
|
||||
{
|
||||
"rule": "env",
|
||||
"variable": "ANDROID_HOME",
|
||||
@@ -32,14 +50,14 @@
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "ruby",
|
||||
"semver": ">=3.2.0",
|
||||
"semver": ">=2.7.1 <3.0.0",
|
||||
"error": "visit rvm install https://rvm.io/rvm/install",
|
||||
"platform": "darwin"
|
||||
},
|
||||
{
|
||||
"rule": "cli",
|
||||
"binary": "pod",
|
||||
"semver": "1.16.1",
|
||||
"semver": "1.11.3",
|
||||
"platform": "darwin"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Submit feature requests to https://mattermost.com/suggestions/. File non-security related bugs here in the following format:
|
||||
Submit feature requests to https://portal.productboard.com/mattermost/33-what-matters-to-you. File non-security related bugs here in the following format:
|
||||
|
||||
#### Summary
|
||||
Issue in one concise sentence.
|
||||
|
||||
1364
NOTICE.txt
1364
NOTICE.txt
File diff suppressed because it is too large
Load Diff
16
README.md
16
README.md
@@ -1,12 +1,12 @@
|
||||
# Mattermost Mobile v2
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (10.5.0+)
|
||||
- **Supported iOS versions:** 15.1+
|
||||
- **Minimum Server versions:** Current ESR version (7.1.0+)
|
||||
- **Supported iOS versions:** 12.1+
|
||||
- **Supported Android versions:** 7.0+
|
||||
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 21 languages. Learn more at [https://mattermost.com](https://mattermost.com).
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 21 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
|
||||
|
||||
You can download our apps from the [App Store](https://mattermost.com/mattermost-ios-app/) or [Google Play Store](https://mattermost.com/mattermost-android-app/), or [build them yourself](https://developers.mattermost.com/contribute/mobile/build-your-own/).
|
||||
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or [build them yourself](https://developers.mattermost.com/contribute/mobile/build-your-own/).
|
||||
|
||||
We plan on releasing monthly updates with new features - check the [changelog](https://github.com/mattermost/mattermost-mobile/blob/master/CHANGELOG.md) for what features are currently supported!
|
||||
|
||||
@@ -27,8 +27,8 @@ To help with testing app updates before they're released, you can:
|
||||
- Repro steps
|
||||
- Observed behavior (including screenshot / video when possible)
|
||||
- Expected behavior
|
||||
4. (Optional) [Sign up for our team site](https://community.mattermost.com/signup_user_complete/?id=codoy5s743rq5mk18i7u5ksz7e&md=link&sbr=su)
|
||||
- Join the [Native Mobile Apps channel](https://community.mattermost.com/core/channels/native-mobile-apps) to see what's new and discuss feedback with other contributors and the core team
|
||||
4. (Optional) [Sign up for our team site](https://pre-release.mattermost.com/signup_user_complete/?id=f1924a8db44ff3bb41c96424cdc20676)
|
||||
- Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to see what's new and discuss feedback with other contributors and the core team
|
||||
|
||||
You can leave the Beta testing program at any time:
|
||||
- On Android, [click this link](https://play.google.com/apps/testing/com.mattermost.rnbeta) while logged in with your Google Play email address used to opt-in for the Beta program, then click **Leave the program**.
|
||||
@@ -39,7 +39,7 @@ You can leave the Beta testing program at any time:
|
||||
1. Look in [GitHub issues](https://mattermost.com/pl/help-wanted-mattermost-mobile) for issues marked as [Help Wanted]
|
||||
2. Comment to let people know you’re working on it
|
||||
3. Follow [these instructions](https://developers.mattermost.com/contribute/mobile/developer-setup/) to set up your developer environment
|
||||
4. Join the [Native Mobile Apps channel](https://community.mattermost.com/core/channels/native-mobile-apps) on our team site to ask questions
|
||||
4. Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) on our team site to ask questions
|
||||
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ You can still access it! We have moved the code from master to the [v1 branch](h
|
||||
|
||||
### 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 there’s an error about the missing chain or certificate path, there is likely an intermediate certificate missing that needs to be included.
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
apply plugin: 'kotlin-android'
|
||||
import com.android.build.OutputFile
|
||||
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
@@ -9,14 +11,14 @@ apply plugin: "com.facebook.react"
|
||||
|
||||
react {
|
||||
/* Folders */
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
||||
// root = file("../../")
|
||||
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
|
||||
// reactNativeDir = file("../../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
|
||||
// codegenDir = file("../../node_modules/@react-native/codegen")
|
||||
// The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
|
||||
// cliFile = file("../../node_modules/react-native/cli.js")
|
||||
// The root of your project, i.e. where "package.json" lives. Default is '..'
|
||||
// root = file("../")
|
||||
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
|
||||
// reactNativeDir = file("../node_modules/react-native")
|
||||
// The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen
|
||||
// codegenDir = file("../node_modules/react-native-codegen")
|
||||
// The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
|
||||
// cliFile = file("../node_modules/react-native/cli.js")
|
||||
/* Variants */
|
||||
// The list of variants to that are debuggable. For those we're going to
|
||||
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
||||
@@ -47,9 +49,6 @@ apply plugin: "com.facebook.react"
|
||||
//
|
||||
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
||||
// hermesFlags = ["-O", "-output-source-map"]
|
||||
|
||||
/* Autolinking */
|
||||
autolinkLibrariesWithApp()
|
||||
}
|
||||
|
||||
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
||||
@@ -70,7 +69,6 @@ if (System.getenv("SENTRY_ENABLED") == "true") {
|
||||
* and want to have separate APKs to upload to the Play Store
|
||||
*/
|
||||
def enableSeparateBuildPerCPUArchitecture = project.hasProperty('separateApk') ? project.property('separateApk').toBoolean() : false
|
||||
def enableUniversalBuild = project.hasProperty('universalApk') ? project.property('universalApk').toBoolean() : false
|
||||
|
||||
/**
|
||||
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
|
||||
@@ -102,7 +100,6 @@ def reactNativeArchitectures() {
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
namespace "com.mattermost.rnbeta"
|
||||
|
||||
@@ -115,8 +112,8 @@ android {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 666
|
||||
versionName "2.32.0"
|
||||
versionCode 457
|
||||
versionName "2.1.0"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
@@ -141,7 +138,7 @@ android {
|
||||
abi {
|
||||
reset()
|
||||
enable enableSeparateBuildPerCPUArchitecture
|
||||
universalApk enableUniversalBuild // If true, also generate a universal APK
|
||||
universalApk enableSeparateBuildPerCPUArchitecture // If true, also generate a universal APK
|
||||
include (*reactNativeArchitectures())
|
||||
}
|
||||
}
|
||||
@@ -150,7 +147,6 @@ android {
|
||||
release {
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"
|
||||
if (useReleaseKey) {
|
||||
signingConfig signingConfigs.release
|
||||
} else {
|
||||
@@ -170,17 +166,16 @@ android {
|
||||
matchingFallbacks = ['release']
|
||||
}
|
||||
}
|
||||
|
||||
// applicationVariants are e.g. debug, release
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
// For each separate APK per architecture, set a unique version code as described here:
|
||||
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
|
||||
def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a": 3, "x86_64": 4]
|
||||
def abi = output.filters[0]
|
||||
def abi = output.getFilter(OutputFile.ABI)
|
||||
if (abi != null) { // null for the universal-debug, universal-release variants
|
||||
output.versionCodeOverride =
|
||||
versionCodes.get(abi.identifier) * 2000000 + defaultConfig.versionCode
|
||||
versionCodes.get(abi) * 2000000 + defaultConfig.versionCode
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,67 +185,64 @@ repositories {
|
||||
maven {
|
||||
url 'https://maven.google.com'
|
||||
}
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
|
||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.squareup.okhttp3', module:'okhttp'
|
||||
}
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}")
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
|
||||
implementation 'io.reactivex.rxjava3:rxjava:3.1.6'
|
||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
|
||||
implementation 'androidx.window:window-rxjava3:1.0.0'
|
||||
implementation 'androidx.window:window:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation "com.google.firebase:firebase-messaging:$firebaseVersion"
|
||||
|
||||
androidTestImplementation('com.wix:detox:+')
|
||||
androidTestImplementation 'androidx.test:core:1.6.1'
|
||||
androidTestImplementation 'androidx.test:runner:1.6.2'
|
||||
androidTestImplementation 'com.wix:detox:20.26.2'
|
||||
|
||||
// For animated GIF support
|
||||
implementation 'com.facebook.fresco:animated-gif:3.6.0'
|
||||
|
||||
// For WebP support, including animated WebP
|
||||
implementation 'com.facebook.fresco:animated-webp:3.6.0'
|
||||
implementation 'com.facebook.fresco:webpsupport:3.6.0'
|
||||
|
||||
implementation project(':reactnativenotifications')
|
||||
implementation project(':watermelondb-jsi')
|
||||
|
||||
api('io.jsonwebtoken:jjwt-api:0.12.5')
|
||||
runtimeOnly('io.jsonwebtoken:jjwt-impl:0.12.5')
|
||||
runtimeOnly('io.jsonwebtoken:jjwt-orgjson:0.12.5') {
|
||||
exclude(group: 'org.json', module: 'json') //provided by Android natively
|
||||
}
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
force 'androidx.test:core:1.6.1'
|
||||
eachDependency { DependencyResolveDetails details ->
|
||||
if (details.requested.name == 'play-services-base') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.5.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-tasks') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.2.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-stats') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-basement') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.5.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'okhttp') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.12.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
|
||||
}
|
||||
if (details.requested.name == 'okhttp-tls') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.12.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
|
||||
}
|
||||
if (details.requested.name == 'okhttp-urlconnection') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.12.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -258,9 +250,10 @@ configurations.all {
|
||||
|
||||
// Run this once to be able to run the application with BUCK
|
||||
// puts all compile dependencies into folder libs for BUCK to use
|
||||
tasks.register('copyDownloadableDepsToLibs', Copy) {
|
||||
task copyDownloadableDepsToLibs(type: Copy) {
|
||||
from configurations.implementation
|
||||
into 'libs'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
||||
|
||||
Binary file not shown.
5
android/app/proguard-rules.pro
vendored
5
android/app/proguard-rules.pro
vendored
@@ -8,8 +8,3 @@
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
-keepattributes InnerClasses
|
||||
|
||||
-keep class io.jsonwebtoken.** { *; }
|
||||
-keepnames class io.jsonwebtoken.* { *; }
|
||||
-keepnames interface io.jsonwebtoken.* { *; }
|
||||
|
||||
@@ -2,8 +2,15 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="28"
|
||||
tools:ignore="GoogleAppIndexingWarning"/>
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<activity
|
||||
android:name="com.facebook.react.devsupport.DevSettingsActivity"
|
||||
android:exported="false"
|
||||
/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.mattermost.flipper;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.flipper.android.AndroidFlipperClient;
|
||||
import com.facebook.flipper.android.utils.FlipperUtils;
|
||||
import com.facebook.flipper.core.FlipperClient;
|
||||
import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
|
||||
import com.facebook.react.ReactInstanceEventListener;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.modules.network.NetworkingModule;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
/**
|
||||
* Class responsible of loading Flipper inside your React Native application. This is the debug
|
||||
* flavor of it. Here you can add your own plugins and customize the Flipper setup.
|
||||
*/
|
||||
public class ReactNativeFlipper {
|
||||
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
|
||||
if (FlipperUtils.shouldEnableFlipper(context)) {
|
||||
final FlipperClient client = AndroidFlipperClient.getInstance(context);
|
||||
client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
|
||||
client.addPlugin(new DatabasesFlipperPlugin(context));
|
||||
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
|
||||
client.addPlugin(CrashReporterPlugin.getInstance());
|
||||
NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
|
||||
NetworkingModule.setCustomClientBuilder(
|
||||
new NetworkingModule.CustomClientBuilder() {
|
||||
@Override
|
||||
public void apply(OkHttpClient.Builder builder) {
|
||||
builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
|
||||
}
|
||||
});
|
||||
client.addPlugin(networkFlipperPlugin);
|
||||
client.start();
|
||||
// Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
|
||||
// Hence we run if after all native modules have been initialized
|
||||
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
|
||||
if (reactContext == null) {
|
||||
reactInstanceManager.addReactInstanceEventListener(
|
||||
new ReactInstanceEventListener() {
|
||||
@Override
|
||||
public void onReactContextInitialized(ReactContext reactContext) {
|
||||
reactInstanceManager.removeReactInstanceEventListener(this);
|
||||
reactContext.runOnNativeModulesQueueThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,18 @@
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission-sdk-23 android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:ignore="SelectedPhotoAccess" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||
|
||||
<!-- Request legacy Bluetooth permissions on older devices. -->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH"
|
||||
@@ -39,8 +32,6 @@
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="false"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
@@ -56,6 +47,7 @@
|
||||
android:resource="@xml/app_restrictions" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
@@ -90,12 +82,14 @@
|
||||
android:name="com.reactnativenavigation.controllers.NavigationActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:resizeableActivity="true"
|
||||
android:exported="false"
|
||||
android:exported="true"
|
||||
/>
|
||||
<activity
|
||||
android:name="com.mattermost.rnshare.ShareActivity"
|
||||
android:name="com.mattermost.share.ShareActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/AppTheme"
|
||||
android:taskAffinity="com.mattermost.share"
|
||||
android:exported="true"
|
||||
@@ -108,22 +102,5 @@
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- For Calls microphone to work in the background -->
|
||||
<service
|
||||
android:name="com.voximplant.foregroundservice.VIForegroundService"
|
||||
android:foregroundServiceType="microphone"
|
||||
android:exported="false"
|
||||
/>
|
||||
|
||||
<!-- Android 14 requires the correct Foreground Service (FGS) type for RNShare -->
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false"
|
||||
tools:node="merge"
|
||||
/>
|
||||
|
||||
</application>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
4
android/app/src/main/assets/certs/.gitignore
vendored
4
android/app/src/main/assets/certs/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
||||
@@ -5,7 +5,6 @@ import android.util.LruCache
|
||||
|
||||
class BitmapCache {
|
||||
private var memoryCache: LruCache<String, Bitmap>
|
||||
private var keysCache: LruCache<String, String>
|
||||
|
||||
init {
|
||||
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
|
||||
@@ -15,35 +14,15 @@ class BitmapCache {
|
||||
return bitmap.byteCount / 1024
|
||||
}
|
||||
}
|
||||
keysCache = LruCache<String, String>(50)
|
||||
}
|
||||
|
||||
fun bitmap(userId: String, updatedAt: Double, serverUrl: String): Bitmap? {
|
||||
val key = "$serverUrl-$userId-$updatedAt"
|
||||
fun getBitmapFromMemCache(key: String): Bitmap? {
|
||||
return memoryCache.get(key)
|
||||
}
|
||||
|
||||
fun insertBitmap(bitmap: Bitmap?, userId: String, updatedAt: Double, serverUrl: String) {
|
||||
if (bitmap == null) {
|
||||
removeBitmap(userId, serverUrl)
|
||||
fun addBitmapToMemoryCache(key: String, bitmap: Bitmap) {
|
||||
if (getBitmapFromMemCache(key) == null) {
|
||||
memoryCache.put(key, bitmap)
|
||||
}
|
||||
val key = "$serverUrl-$userId-$updatedAt"
|
||||
val cachedKey = "$serverUrl-$userId"
|
||||
keysCache.put(cachedKey, key)
|
||||
memoryCache.put(key, bitmap)
|
||||
}
|
||||
|
||||
fun removeBitmap(userId: String, serverUrl: String) {
|
||||
val cachedKey = "$serverUrl-$userId"
|
||||
val key = keysCache.get(cachedKey)
|
||||
if (key != null) {
|
||||
memoryCache.remove(key)
|
||||
keysCache.remove(cachedKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllBitmaps() {
|
||||
memoryCache.evictAll()
|
||||
keysCache.evictAll()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.mattermost.helpers;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.oblador.keychain.KeychainModule;
|
||||
|
||||
|
||||
public class Credentials {
|
||||
public static void getCredentialsForServer(ReactApplicationContext context, String serverUrl, ResolvePromise promise) {
|
||||
final KeychainModule keychainModule = new KeychainModule(context);
|
||||
|
||||
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 String getCredentialsForServerSync(ReactApplicationContext context, String serverUrl) {
|
||||
final String[] token = new String[1];
|
||||
Credentials.getCredentialsForServer(context, serverUrl, new ResolvePromise() {
|
||||
@Override
|
||||
public void resolve(@Nullable Object value) {
|
||||
WritableMap map = (WritableMap) value;
|
||||
if (map != null) {
|
||||
token[0] = map.getString("password");
|
||||
String service = map.getString("service");
|
||||
assert service != null;
|
||||
if (service.isEmpty()) {
|
||||
String[] credentials = token[0].split(",[ ]*");
|
||||
if (credentials.length == 2) {
|
||||
token[0] = credentials[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return token[0];
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,8 @@ import android.graphics.RectF;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.app.Person;
|
||||
@@ -29,41 +28,25 @@ import androidx.core.app.RemoteInput;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
|
||||
import com.mattermost.rnbeta.*;
|
||||
import com.mattermost.rnutils.helpers.NotificationHelper;
|
||||
import com.nozbe.watermelondb.WMDatabase;
|
||||
import com.mattermost.turbolog.TurboLog;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.jsonwebtoken.IncorrectClaimException;
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.MissingClaimException;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
import static com.mattermost.helpers.database_extension.GeneralKt.getDatabaseForServer;
|
||||
import static com.mattermost.helpers.database_extension.GeneralKt.getDeviceToken;
|
||||
import static com.mattermost.helpers.database_extension.SystemKt.queryConfigServerVersion;
|
||||
import static com.mattermost.helpers.database_extension.SystemKt.queryConfigSigningKey;
|
||||
import static com.mattermost.helpers.database_extension.UserKt.getLastPictureUpdate;
|
||||
|
||||
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";
|
||||
public static final String NOTIFICATION = "notification";
|
||||
public static final String PUSH_TYPE_MESSAGE = "message";
|
||||
public static final String PUSH_TYPE_CLEAR = "clear";
|
||||
public static final String PUSH_TYPE_SESSION = "session";
|
||||
public static final String CATEGORY_CAN_REPLY = "CAN_REPLY";
|
||||
|
||||
private static NotificationChannel mHighImportanceChannel;
|
||||
private static NotificationChannel mMinImportanceChannel;
|
||||
@@ -72,7 +55,7 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
private static final BitmapCache bitmapCache = new BitmapCache();
|
||||
|
||||
private static void addMessagingStyleMessages(Context context, NotificationCompat.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
|
||||
private static void addMessagingStyleMessages(NotificationCompat.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
|
||||
String message = bundle.getString("message", bundle.getString("body"));
|
||||
String senderId = bundle.getString("sender_id");
|
||||
String serverUrl = bundle.getString("server_url");
|
||||
@@ -92,9 +75,9 @@ public class CustomPushNotificationHelper {
|
||||
.setKey(senderId)
|
||||
.setName(senderName);
|
||||
|
||||
if (serverUrl != null && type != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
|
||||
Bitmap avatar = userAvatar(serverUrl, senderId, urlOverride);
|
||||
if (avatar != null) {
|
||||
sender.setIcon(IconCompat.createWithBitmap(avatar));
|
||||
}
|
||||
@@ -140,13 +123,11 @@ public class CustomPushNotificationHelper {
|
||||
notification.addExtras(userInfoBundle);
|
||||
}
|
||||
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
private static void addNotificationReplyAction(Context context, NotificationCompat.Builder notification, Bundle bundle, int notificationId) {
|
||||
String postId = bundle.getString("post_id");
|
||||
String serverUrl = bundle.getString("server_url");
|
||||
boolean canReply = bundle.containsKey("category") && Objects.equals(bundle.getString("category"), CATEGORY_CAN_REPLY);
|
||||
|
||||
if (android.text.TextUtils.isEmpty(postId) || serverUrl == null || !canReply) {
|
||||
if (android.text.TextUtils.isEmpty(postId) || serverUrl == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -192,14 +173,14 @@ public class CustomPushNotificationHelper {
|
||||
String channelId = bundle.getString("channel_id");
|
||||
String postId = bundle.getString("post_id");
|
||||
String rootId = bundle.getString("root_id");
|
||||
int notificationId = postId != null ? postId.hashCode() : NotificationHelper.INSTANCE.MESSAGE_NOTIFICATION_ID;
|
||||
int notificationId = postId != null ? postId.hashCode() : MESSAGE_NOTIFICATION_ID;
|
||||
|
||||
boolean is_crt_enabled = bundle.containsKey("is_crt_enabled") && Objects.equals(bundle.getString("is_crt_enabled"), "true");
|
||||
boolean is_crt_enabled = bundle.containsKey("is_crt_enabled") && bundle.getString("is_crt_enabled").equals("true");
|
||||
String groupId = is_crt_enabled && !android.text.TextUtils.isEmpty(rootId) ? rootId : channelId;
|
||||
|
||||
addNotificationExtras(notification, bundle);
|
||||
setNotificationIcons(context, notification, bundle);
|
||||
setNotificationMessagingStyle(context, notification, bundle);
|
||||
setNotificationIcons(notification, bundle);
|
||||
setNotificationMessagingStyle(notification, bundle);
|
||||
setNotificationGroup(notification, groupId, createSummary);
|
||||
setNotificationBadgeType(notification);
|
||||
|
||||
@@ -238,154 +219,6 @@ public class CustomPushNotificationHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean verifySignature(final Context context, String signature, String serverUrl, String ackId) {
|
||||
if (signature == null) {
|
||||
// Backward compatibility with old push proxies
|
||||
TurboLog.Companion.i("Mattermost Notifications Signature verification", "No signature in the notification");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (serverUrl == null) {
|
||||
TurboLog.Companion.i("Mattermost Notifications Signature verification", "No server_url for server_id");
|
||||
return false;
|
||||
}
|
||||
|
||||
DatabaseHelper dbHelper = DatabaseHelper.Companion.getInstance();
|
||||
if (dbHelper == null) {
|
||||
TurboLog.Companion.i("Mattermost Notifications Signature verification", "Cannot access the database");
|
||||
return false;
|
||||
}
|
||||
|
||||
WMDatabase db = getDatabaseForServer(dbHelper, context, serverUrl);
|
||||
if (db == null) {
|
||||
TurboLog.Companion.i("Mattermost Notifications Signature verification", "Cannot access the server database");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (signature.equals("NO_SIGNATURE")) {
|
||||
String version = queryConfigServerVersion(db);
|
||||
if (version == null) {
|
||||
TurboLog.Companion.i("Mattermost Notifications Signature verification", "No server version");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!version.matches("[0-9]+(\\.[0-9]+)*")) {
|
||||
TurboLog.Companion.i("Mattermost Notifications Signature verification", "Invalid server version");
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] parts = version.split("\\.");
|
||||
int major = parts.length > 0 ? Integer.parseInt(parts[0]) : 0;
|
||||
int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0;
|
||||
int patch = parts.length > 2 ? Integer.parseInt(parts[2]) : 0;
|
||||
|
||||
int[][] targets = {{9,8,0},{9,7,3},{9,6,3},{9,5,5},{8,1,14}};
|
||||
boolean rejected = false;
|
||||
for (int i = 0; i < targets.length; i++) {
|
||||
boolean first = i == 0;
|
||||
int[] targetVersion = targets[i];
|
||||
int majorTarget = targetVersion[0];
|
||||
int minorTarget = targetVersion[1];
|
||||
int patchTarget = targetVersion[2];
|
||||
|
||||
if (major > majorTarget) {
|
||||
// Only reject if we are considering the first (highest) version.
|
||||
// Any version in between should be acceptable.
|
||||
rejected = first;
|
||||
break;
|
||||
}
|
||||
|
||||
if (major < majorTarget) {
|
||||
// Continue to see if it complies with a smaller target
|
||||
continue;
|
||||
}
|
||||
|
||||
// Same major
|
||||
if (minor > minorTarget) {
|
||||
// Only reject if we are considering the first (highest) version.
|
||||
// Any version in between should be acceptable.
|
||||
rejected = first;
|
||||
break;
|
||||
}
|
||||
|
||||
if (minor < minorTarget) {
|
||||
// Continue to see if it complies with a smaller target
|
||||
continue;
|
||||
}
|
||||
|
||||
// Same major and same minor
|
||||
if (patch >= patchTarget) {
|
||||
rejected = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Patch is lower than target
|
||||
return true;
|
||||
}
|
||||
|
||||
if (rejected) {
|
||||
TurboLog.Companion.i("Mattermost Notifications Signature verification", "Server version should send signature");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Version number is below any of the targets, so it should not send the signature
|
||||
return true;
|
||||
}
|
||||
|
||||
String signingKey = queryConfigSigningKey(db);
|
||||
if (signingKey == null) {
|
||||
TurboLog.Companion.i("Mattermost Notifications Signature verification", "No signing key");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] encoded = Base64.decode(signingKey, 0);
|
||||
KeyFactory kf = KeyFactory.getInstance("EC");
|
||||
PublicKey pubKey = (PublicKey) kf.generatePublic(new X509EncodedKeySpec(encoded));
|
||||
|
||||
String storedDeviceToken = getDeviceToken(dbHelper);
|
||||
if (storedDeviceToken == null) {
|
||||
TurboLog.Companion.i("Mattermost Notifications Signature verification", "No device token stored");
|
||||
return false;
|
||||
}
|
||||
String[] tokenParts = storedDeviceToken.split(":", 2);
|
||||
if (tokenParts.length != 2) {
|
||||
TurboLog.Companion.i("Mattermost Notifications Signature verification", "Wrong stored device token format");
|
||||
return false;
|
||||
}
|
||||
String deviceToken = tokenParts[1].substring(0, tokenParts[1].length() -1 );
|
||||
if (deviceToken.isEmpty()) {
|
||||
TurboLog.Companion.i("Mattermost Notifications Signature verification", "Empty stored device token");
|
||||
return false;
|
||||
}
|
||||
|
||||
Jwts.parser()
|
||||
.require("ack_id", ackId)
|
||||
.require("device_id", deviceToken)
|
||||
.verifyWith((PublicKey) pubKey)
|
||||
.build()
|
||||
.parseSignedClaims(signature);
|
||||
} catch (MissingClaimException e) {
|
||||
TurboLog.Companion.i("Mattermost Notifications Signature verification", String.format("Missing claim: %s", e.getMessage()));
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
} catch (IncorrectClaimException e) {
|
||||
TurboLog.Companion.i("Mattermost Notifications Signature verification", String.format("Incorrect claim: %s", e.getMessage()));
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
} catch (JwtException e) {
|
||||
TurboLog.Companion.i("Mattermost Notifications Signature verification", String.format("Cannot verify JWT: %s", e.getMessage()));
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
TurboLog.Companion.i("Mattermost Notifications Signature verification", String.format("Exception while parsing JWT: %s", e.getMessage()));
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Bitmap getCircleBitmap(Bitmap bitmap) {
|
||||
final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),
|
||||
bitmap.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
@@ -423,7 +256,7 @@ public class CustomPushNotificationHelper {
|
||||
return title;
|
||||
}
|
||||
|
||||
private static NotificationCompat.MessagingStyle getMessagingStyle(Context context, Bundle bundle) {
|
||||
private static NotificationCompat.MessagingStyle getMessagingStyle(Bundle bundle) {
|
||||
NotificationCompat.MessagingStyle messagingStyle;
|
||||
final String senderId = "me";
|
||||
final String serverUrl = bundle.getString("server_url");
|
||||
@@ -434,9 +267,9 @@ public class CustomPushNotificationHelper {
|
||||
.setKey(senderId)
|
||||
.setName("Me");
|
||||
|
||||
if (serverUrl != null && type != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
Bitmap avatar = userAvatar(context, serverUrl, "me", urlOverride);
|
||||
Bitmap avatar = userAvatar(serverUrl, "me", urlOverride);
|
||||
if (avatar != null) {
|
||||
sender.setIcon(IconCompat.createWithBitmap(avatar));
|
||||
}
|
||||
@@ -449,7 +282,7 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
String conversationTitle = getConversationTitle(bundle);
|
||||
setMessagingStyleConversationTitle(messagingStyle, conversationTitle, bundle);
|
||||
addMessagingStyleMessages(context, messagingStyle, conversationTitle, bundle);
|
||||
addMessagingStyleMessages(messagingStyle, conversationTitle, bundle);
|
||||
|
||||
return messagingStyle;
|
||||
}
|
||||
@@ -531,8 +364,8 @@ public class CustomPushNotificationHelper {
|
||||
notification.setDeleteIntent(deleteIntent);
|
||||
}
|
||||
|
||||
private static void setNotificationMessagingStyle(Context context, NotificationCompat.Builder notification, Bundle bundle) {
|
||||
NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(context, bundle);
|
||||
private static void setNotificationMessagingStyle(NotificationCompat.Builder notification, Bundle bundle) {
|
||||
NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(bundle);
|
||||
notification.setStyle(messagingStyle);
|
||||
}
|
||||
|
||||
@@ -545,7 +378,7 @@ public class CustomPushNotificationHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static void setNotificationIcons(Context context, NotificationCompat.Builder notification, Bundle bundle) {
|
||||
private static void setNotificationIcons(NotificationCompat.Builder notification, Bundle bundle) {
|
||||
String channelName = getConversationTitle(bundle);
|
||||
String senderName = bundle.getString("sender_name");
|
||||
String serverUrl = bundle.getString("server_url");
|
||||
@@ -556,7 +389,7 @@ public class CustomPushNotificationHelper {
|
||||
if (serverUrl != null && channelName.equals(senderName)) {
|
||||
try {
|
||||
String senderId = bundle.getString("sender_id");
|
||||
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
|
||||
Bitmap avatar = userAvatar(serverUrl, senderId, urlOverride);
|
||||
if (avatar != null) {
|
||||
notification.setLargeIcon(avatar);
|
||||
}
|
||||
@@ -566,35 +399,21 @@ public class CustomPushNotificationHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static Bitmap userAvatar(final Context context, @NonNull final String serverUrl, final String userId, final String urlOverride) throws IOException {
|
||||
private static Bitmap userAvatar(final String serverUrl, final String userId, final String urlOverride) throws IOException {
|
||||
try {
|
||||
Response response;
|
||||
Double lastUpdateAt = 0.0;
|
||||
if (!TextUtils.isEmpty(urlOverride)) {
|
||||
Request request = new Request.Builder().url(urlOverride).build();
|
||||
TurboLog.Companion.i("ReactNative", String.format("Fetch override profile image %s", urlOverride));
|
||||
Log.i("ReactNative", String.format("Fetch override profile image %s", urlOverride));
|
||||
response = client.newCall(request).execute();
|
||||
} else {
|
||||
DatabaseHelper dbHelper = DatabaseHelper.Companion.getInstance();
|
||||
if (dbHelper != null) {
|
||||
WMDatabase db = getDatabaseForServer(dbHelper, context, serverUrl);
|
||||
if (db != null) {
|
||||
lastUpdateAt = getLastPictureUpdate(db, userId);
|
||||
if (lastUpdateAt == null) {
|
||||
lastUpdateAt = 0.0;
|
||||
}
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
Bitmap cached = bitmapCache.bitmap(userId, lastUpdateAt, serverUrl);
|
||||
Bitmap cached = bitmapCache.getBitmapFromMemCache(userId);
|
||||
if (cached != null) {
|
||||
Bitmap bitmap = cached.copy(cached.getConfig(), false);
|
||||
return getCircleBitmap(bitmap);
|
||||
}
|
||||
|
||||
bitmapCache.removeBitmap(userId, serverUrl);
|
||||
String url = String.format("api/v4/users/%s/image", userId);
|
||||
TurboLog.Companion.i("ReactNative", String.format("Fetch profile image %s", url));
|
||||
Log.i("ReactNative", String.format("Fetch profile image %s", url));
|
||||
response = Network.getSync(serverUrl, url, null);
|
||||
}
|
||||
|
||||
@@ -603,7 +422,7 @@ public class CustomPushNotificationHelper {
|
||||
byte[] bytes = Objects.requireNonNull(response.body()).bytes();
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
|
||||
if (TextUtils.isEmpty(urlOverride) && !TextUtils.isEmpty(userId)) {
|
||||
bitmapCache.insertBitmap(bitmap.copy(bitmap.getConfig(), false), userId, lastUpdateAt, serverUrl);
|
||||
bitmapCache.addBitmapToMemoryCache(userId, bitmap.copy(bitmap.getConfig(), false));
|
||||
}
|
||||
return getCircleBitmap(bitmap);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
package com.mattermost.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import com.facebook.react.bridge.WritableMap
|
||||
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
|
||||
import java.lang.Exception
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
typealias QueryArgs = Array<Any?>
|
||||
import java.lang.Exception
|
||||
import java.util.*
|
||||
|
||||
class DatabaseHelper {
|
||||
var defaultDatabase: WMDatabase? = null
|
||||
private var defaultDatabase: Database? = null
|
||||
|
||||
val onlyServerUrl: String?
|
||||
get() {
|
||||
try {
|
||||
val query = "SELECT url FROM Servers WHERE last_active_at != 0 AND identifier != ''"
|
||||
defaultDatabase!!.rawQuery(query).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getString(0)
|
||||
}
|
||||
val cursor = defaultDatabase!!.rawQuery(query)
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val url = cursor.getString(0)
|
||||
cursor.close()
|
||||
return url
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
@@ -40,13 +42,640 @@ class DatabaseHelper {
|
||||
}
|
||||
}
|
||||
|
||||
fun getServerUrlForIdentifier(identifier: String): String? {
|
||||
try {
|
||||
val args: Array<Any?> = arrayOf(identifier)
|
||||
val query = "SELECT url FROM Servers WHERE identifier=?"
|
||||
val cursor = defaultDatabase!!.rawQuery(query, args)
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val url = cursor.getString(0)
|
||||
cursor.close()
|
||||
return url
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun find(db: Database, tableName: String, id: String?): ReadableMap? {
|
||||
val args: Array<Any?> = arrayOf(id)
|
||||
try {
|
||||
db.rawQuery("select * from $tableName where id == ? limit 1", args).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return null
|
||||
}
|
||||
val resultMap = Arguments.createMap()
|
||||
cursor.moveToFirst()
|
||||
resultMap.mapCursor(cursor)
|
||||
return resultMap
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun getDatabaseForServer(context: Context?, serverUrl: String): Database? {
|
||||
try {
|
||||
val args: Array<Any?> = arrayOf(serverUrl)
|
||||
val query = "SELECT db_path FROM Servers WHERE url=?"
|
||||
val cursor = defaultDatabase!!.rawQuery(query, args)
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val databasePath = cursor.getString(0)
|
||||
cursor.close()
|
||||
return Database(databasePath, context!!)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun queryIds(db: Database, tableName: String, ids: Array<String>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(ids).map { "?" }.toArray())
|
||||
try {
|
||||
db.rawQuery("select distinct id from $tableName where id IN ($args)", ids as Array<Any?>).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex("id")
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun queryByColumn(db: Database, tableName: String, columnName: String, values: Array<Any?>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(values).map { "?" }.toArray())
|
||||
try {
|
||||
db.rawQuery("select distinct $columnName from $tableName where $columnName IN ($args)", values).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex(columnName)
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun queryCurrentUserId(db: Database): String? {
|
||||
val result = find(db, "System", "currentUserId")!!
|
||||
return result.getString("value")
|
||||
}
|
||||
|
||||
private fun queryLastPostCreateAt(db: Database?, channelId: String): Double? {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT earliest, latest FROM PostsInChannel WHERE channel_id=? ORDER BY latest DESC LIMIT 1"
|
||||
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
|
||||
if (cursor1.count == 1) {
|
||||
cursor1.moveToFirst()
|
||||
val earliest = cursor1.getDouble(0)
|
||||
val latest = cursor1.getDouble(1)
|
||||
cursor1.close()
|
||||
val postQuery = "SELECT create_at FROM POST WHERE channel_id= ? AND delete_at=0 AND create_at BETWEEN ? AND ? ORDER BY create_at DESC"
|
||||
val cursor2 = db.rawQuery(postQuery, arrayOf(channelId, earliest, latest))
|
||||
if (cursor2.count >= 60) {
|
||||
cursor2.moveToFirst()
|
||||
val createAt = cursor2.getDouble(0)
|
||||
cursor2.close()
|
||||
return createAt
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun queryPostSinceForChannel(db: Database?, channelId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
|
||||
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
|
||||
if (cursor1.count == 1) {
|
||||
cursor1.moveToFirst()
|
||||
val lastFetchedAt = cursor1.getDouble(0)
|
||||
cursor1.close()
|
||||
if (lastFetchedAt == 0.0) {
|
||||
return queryLastPostCreateAt(db, channelId)
|
||||
}
|
||||
return lastFetchedAt
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun handlePosts(db: Database, postsData: ReadableMap?, channelId: String, receivingThreads: Boolean) {
|
||||
// Posts, PostInChannel, PostInThread, Reactions, Files, CustomEmojis, Users
|
||||
if (postsData != null) {
|
||||
val ordered = postsData.getArray("order")?.toArrayList()
|
||||
val posts = ReadableMapUtils.toJSONObject(postsData.getMap("posts")).toMap()
|
||||
val previousPostId = postsData.getString("prev_post_id")
|
||||
val postsInThread = hashMapOf<String, List<JSONObject>>()
|
||||
val postList = posts.toList()
|
||||
var earliest = 0.0
|
||||
var latest = 0.0
|
||||
var lastFetchedAt = 0.0
|
||||
|
||||
if (ordered != null && posts.isNotEmpty()) {
|
||||
val firstId = ordered.first()
|
||||
val lastId = ordered.last()
|
||||
lastFetchedAt = postList.fold(0.0) { acc, next ->
|
||||
val post = next.second as Map<*, *>
|
||||
val createAt = post["create_at"] as Double
|
||||
val updateAt = post["update_at"] as Double
|
||||
val deleteAt = post["delete_at"] as Double
|
||||
val value = maxOf(createAt, updateAt, deleteAt)
|
||||
|
||||
maxOf(value, acc)
|
||||
}
|
||||
var prevPostId = ""
|
||||
|
||||
val sortedPosts = postList.sortedBy { (_, value) ->
|
||||
((value as Map<*, *>)["create_at"] as Double)
|
||||
}
|
||||
|
||||
sortedPosts.forEachIndexed { index, it ->
|
||||
val key = it.first
|
||||
if (it.second != null) {
|
||||
val post = it.second as MutableMap<String, Any?>
|
||||
|
||||
if (index == 0) {
|
||||
post.putIfAbsent("prev_post_id", previousPostId)
|
||||
} else if (prevPostId.isNotEmpty()) {
|
||||
post.putIfAbsent("prev_post_id", prevPostId)
|
||||
}
|
||||
|
||||
if (lastId == key) {
|
||||
earliest = post["create_at"] as Double
|
||||
}
|
||||
if (firstId == key) {
|
||||
latest = post["create_at"] as Double
|
||||
}
|
||||
|
||||
val jsonPost = JSONObject(post)
|
||||
val rootId = post["root_id"] as? String
|
||||
|
||||
if (!rootId.isNullOrEmpty()) {
|
||||
var thread = postsInThread[rootId]?.toMutableList()
|
||||
if (thread == null) {
|
||||
thread = mutableListOf()
|
||||
}
|
||||
|
||||
thread.add(jsonPost)
|
||||
postsInThread[rootId] = thread.toList()
|
||||
}
|
||||
|
||||
if (find(db, "Post", key) == null) {
|
||||
insertPost(db, jsonPost)
|
||||
} else {
|
||||
updatePost(db, jsonPost)
|
||||
}
|
||||
|
||||
if (ordered.contains(key)) {
|
||||
prevPostId = key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!receivingThreads) {
|
||||
handlePostsInChannel(db, channelId, earliest, latest)
|
||||
updateMyChannelLastFetchedAt(db, channelId, lastFetchedAt)
|
||||
}
|
||||
handlePostsInThread(db, postsInThread)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleThreads(db: Database, threads: ReadableArray) {
|
||||
for (i in 0 until threads.size()) {
|
||||
val thread = threads.getMap(i)
|
||||
val threadId = thread.getString("id")
|
||||
|
||||
// Insert/Update the thread
|
||||
val existingRecord = find(db, "Thread", threadId)
|
||||
if (existingRecord == null) {
|
||||
insertThread(db, thread)
|
||||
} else {
|
||||
updateThread(db, thread, existingRecord)
|
||||
}
|
||||
|
||||
// Delete existing and insert thread participants
|
||||
val participants = thread.getArray("participants")
|
||||
if (participants != null) {
|
||||
db.execute("delete from ThreadParticipant where thread_id = ?", arrayOf(threadId))
|
||||
|
||||
if (participants.size() > 0) {
|
||||
insertThreadParticipants(db, threadId!!, participants)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleUsers(db: Database, users: ReadableArray) {
|
||||
for (i in 0 until users.size()) {
|
||||
val user = users.getMap(i)
|
||||
val roles = user.getString("roles") ?: ""
|
||||
val isBot = try {
|
||||
user.getBoolean("is_bot")
|
||||
} catch (e: NoSuchKeyException) {
|
||||
false
|
||||
}
|
||||
|
||||
val lastPictureUpdate = try { user.getDouble("last_picture_update") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
|
||||
db.execute(
|
||||
"insert into User (id, auth_service, update_at, delete_at, email, first_name, is_bot, is_guest, " +
|
||||
"last_name, last_picture_update, locale, nickname, position, roles, status, username, notify_props, " +
|
||||
"props, timezone, _status) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
user.getString("id"),
|
||||
user.getString("auth_service"),
|
||||
user.getDouble("update_at"),
|
||||
user.getDouble("delete_at"),
|
||||
user.getString("email"),
|
||||
user.getString("first_name"),
|
||||
isBot,
|
||||
roles.contains("system_guest"),
|
||||
user.getString("last_name"),
|
||||
lastPictureUpdate,
|
||||
user.getString("locale"),
|
||||
user.getString("nickname"),
|
||||
user.getString("position"),
|
||||
roles,
|
||||
"",
|
||||
user.getString("username"),
|
||||
"{}",
|
||||
ReadableMapUtils.toJSONObject(user.getMap("props") ?: Arguments.createMap()).toString(),
|
||||
ReadableMapUtils.toJSONObject(user.getMap("timezone") ?: Arguments.createMap()).toString(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDefaultDatabase(context: Context) {
|
||||
val databaseName = "app.db"
|
||||
val databasePath = Uri.fromFile(context.filesDir).toString() + "/" + databaseName
|
||||
defaultDatabase = WMDatabase.getInstance(databasePath, context)
|
||||
defaultDatabase = Database(databasePath, context)
|
||||
}
|
||||
|
||||
internal fun JSONObject.toMap(): Map<String, Any?> = keys().asSequence().associateWith { it ->
|
||||
private fun insertPost(db: Database, post: JSONObject) {
|
||||
var metadata: JSONObject?
|
||||
var reactions: JSONArray? = null
|
||||
var customEmojis: JSONArray? = null
|
||||
var files: JSONArray? = null
|
||||
|
||||
try {
|
||||
metadata = post.getJSONObject("metadata")
|
||||
reactions = metadata.remove("reactions") as JSONArray?
|
||||
customEmojis = metadata.remove("emojis") as JSONArray?
|
||||
files = metadata.remove("files") as JSONArray?
|
||||
} catch (e: Exception) {
|
||||
// no metadata found
|
||||
metadata = JSONObject()
|
||||
}
|
||||
|
||||
db.execute(
|
||||
"insert into Post " +
|
||||
"(id, channel_id, create_at, delete_at, update_at, edit_at, is_pinned, message, metadata, original_id, pending_post_id, " +
|
||||
"previous_post_id, root_id, type, user_id, props, _status)" +
|
||||
" values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
post.getString("id"),
|
||||
post.getString("channel_id"),
|
||||
post.getDouble("create_at"),
|
||||
post.getDouble("delete_at"),
|
||||
post.getDouble("update_at"),
|
||||
post.getDouble("edit_at"),
|
||||
post.getBoolean("is_pinned"),
|
||||
post.getString("message"),
|
||||
metadata.toString(),
|
||||
post.getString("original_id"),
|
||||
post.getString("pending_post_id"),
|
||||
post.getString("prev_post_id"),
|
||||
post.getString("root_id"),
|
||||
post.getString("type"),
|
||||
post.getString("user_id"),
|
||||
post.getJSONObject("props").toString()
|
||||
)
|
||||
)
|
||||
|
||||
if (reactions != null && reactions.length() > 0) {
|
||||
insertReactions(db, reactions)
|
||||
}
|
||||
|
||||
if (customEmojis != null && customEmojis.length() > 0) {
|
||||
insertCustomEmojis(db, customEmojis)
|
||||
}
|
||||
|
||||
if (files != null && files.length() > 0) {
|
||||
insertFiles(db, files)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePost(db: Database, post: JSONObject) {
|
||||
var metadata: JSONObject?
|
||||
var reactions: JSONArray? = null
|
||||
var customEmojis: JSONArray? = null
|
||||
|
||||
try {
|
||||
metadata = post.getJSONObject("metadata")
|
||||
reactions = metadata.remove("reactions") as JSONArray?
|
||||
customEmojis = metadata.remove("emojis") as JSONArray?
|
||||
metadata.remove("files")
|
||||
} catch (e: Exception) {
|
||||
// no metadata found
|
||||
metadata = JSONObject()
|
||||
}
|
||||
|
||||
db.execute(
|
||||
"update Post SET channel_id = ?, create_at = ?, delete_at = ?, update_at =?, edit_at =?, " +
|
||||
"is_pinned = ?, message = ?, metadata = ?, original_id = ?, pending_post_id = ?, previous_post_id = ?, " +
|
||||
"root_id = ?, type = ?, user_id = ?, props = ?, _status = 'updated' " +
|
||||
"where id = ?",
|
||||
arrayOf(
|
||||
post.getString("channel_id"),
|
||||
post.getDouble("create_at"),
|
||||
post.getDouble("delete_at"),
|
||||
post.getDouble("update_at"),
|
||||
post.getDouble("edit_at"),
|
||||
post.getBoolean("is_pinned"),
|
||||
post.getString("message"),
|
||||
metadata.toString(),
|
||||
post.getString("original_id"),
|
||||
post.getString("pending_post_id"),
|
||||
post.getString("prev_post_id"),
|
||||
post.getString("root_id"),
|
||||
post.getString("type"),
|
||||
post.getString("user_id"),
|
||||
post.getJSONObject("props").toString(),
|
||||
post.getString("id"),
|
||||
)
|
||||
)
|
||||
|
||||
if (reactions != null && reactions.length() > 0) {
|
||||
db.execute("delete from Reaction where post_id = ?", arrayOf(post.getString("id")))
|
||||
insertReactions(db, reactions)
|
||||
}
|
||||
|
||||
if (customEmojis != null && customEmojis.length() > 0) {
|
||||
insertCustomEmojis(db, customEmojis)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertThread(db: Database, thread: ReadableMap) {
|
||||
// These fields are not present when we extract threads from posts
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { false }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { 0 }
|
||||
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"insert into Thread " +
|
||||
"(id, last_reply_at, last_fetched_at, last_viewed_at, reply_count, is_following, unread_replies, unread_mentions, _status)" +
|
||||
" values (?, ?, 0, ?, ?, ?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
thread.getString("id"),
|
||||
lastReplyAt,
|
||||
lastViewedAt,
|
||||
replyCount,
|
||||
isFollowing,
|
||||
unreadReplies,
|
||||
unreadMentions
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateThread(db: Database, thread: ReadableMap, existingRecord: ReadableMap) {
|
||||
// These fields are not present when we extract threads from posts
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { existingRecord.getInt("is_following") == 1 }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { existingRecord.getDouble("last_viewed_at") }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_replies") }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_mentions") }
|
||||
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"update Thread SET last_reply_at = ?, last_viewed_at = ?, reply_count = ?, is_following = ?, unread_replies = ?, unread_mentions = ?, _status = 'updated' where id = ?",
|
||||
arrayOf(
|
||||
lastReplyAt,
|
||||
lastViewedAt,
|
||||
replyCount,
|
||||
isFollowing,
|
||||
unreadReplies,
|
||||
unreadMentions,
|
||||
thread.getString("id")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun insertThreadParticipants(db: Database, threadId: String, participants: ReadableArray) {
|
||||
for (i in 0 until participants.size()) {
|
||||
val participant = participants.getMap(i)
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"insert into ThreadParticipant " +
|
||||
"(id, thread_id, user_id, _status)" +
|
||||
" values (?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
id,
|
||||
threadId,
|
||||
participant.getString("id")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertCustomEmojis(db: Database, customEmojis: JSONArray) {
|
||||
for (i in 0 until customEmojis.length()) {
|
||||
val emoji = customEmojis.getJSONObject(i)
|
||||
if(find(db, "CustomEmoji", emoji.getString("id")) == null) {
|
||||
db.execute(
|
||||
"insert into CustomEmoji (id, name, _status) values (?, ?, 'created')",
|
||||
arrayOf(
|
||||
emoji.getString("id"),
|
||||
emoji.getString("name"),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertFiles(db: Database, files: JSONArray) {
|
||||
for (i in 0 until files.length()) {
|
||||
val file = files.getJSONObject(i)
|
||||
val miniPreview = try { file.getString("mini_preview") } catch (e: JSONException) { "" }
|
||||
val height = try { file.getInt("height") } catch (e: JSONException) { 0 }
|
||||
val width = try { file.getInt("width") } catch (e: JSONException) { 0 }
|
||||
db.execute(
|
||||
"insert into File (id, extension, height, image_thumbnail, local_path, mime_type, name, post_id, size, width, _status) " +
|
||||
"values (?, ?, ?, ?, '', ?, ?, ?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
file.getString("id"),
|
||||
file.getString("extension"),
|
||||
height,
|
||||
miniPreview,
|
||||
file.getString("mime_type"),
|
||||
file.getString("name"),
|
||||
file.getString("post_id"),
|
||||
file.getDouble("size"),
|
||||
width
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertReactions(db: Database, reactions: JSONArray) {
|
||||
for (i in 0 until reactions.length()) {
|
||||
val reaction = reactions.getJSONObject(i)
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"insert into Reaction (id, create_at, emoji_name, post_id, user_id, _status) " +
|
||||
"values (?, ?, ?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
id,
|
||||
reaction.getDouble("create_at"),
|
||||
reaction.getString("emoji_name"),
|
||||
reaction.getString("post_id"),
|
||||
reaction.getString("user_id")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePostsInChannel(db: Database, channelId: String, earliest: Double, latest: Double) {
|
||||
db.rawQuery("select id, channel_id, earliest, latest from PostsInChannel where channel_id = ?", arrayOf(channelId)).use { cursor ->
|
||||
if (cursor.count == 0) {
|
||||
// create new post in channel
|
||||
insertPostInChannel(db, channelId, earliest, latest)
|
||||
return
|
||||
}
|
||||
|
||||
val resultArray = Arguments.createArray()
|
||||
while (cursor.moveToNext()) {
|
||||
val cursorMap = Arguments.createMap()
|
||||
cursorMap.mapCursor(cursor)
|
||||
resultArray.pushMap(cursorMap)
|
||||
}
|
||||
|
||||
val chunk = findPostInChannel(resultArray, earliest, latest)
|
||||
if (chunk != null) {
|
||||
db.execute(
|
||||
"update PostsInChannel set earliest = ?, latest = ?, _status = 'updated' where id = ?",
|
||||
arrayOf(
|
||||
minOf(earliest, chunk.getDouble("earliest")),
|
||||
maxOf(latest, chunk.getDouble("latest")),
|
||||
chunk.getString("id")
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val newChunk = insertPostInChannel(db, channelId, earliest, latest)
|
||||
mergePostsInChannel(db, resultArray, newChunk)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMyChannelLastFetchedAt(db: Database, channelId: String, lastFetchedAt: Double) {
|
||||
db.execute(
|
||||
"UPDATE MyChannel SET last_fetched_at = ?, _status = 'updated' WHERE id = ?",
|
||||
arrayOf(
|
||||
lastFetchedAt,
|
||||
channelId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun findPostInChannel(chunks: ReadableArray, earliest: Double, latest: Double): ReadableMap? {
|
||||
for (i in 0 until chunks.size()) {
|
||||
val chunk = chunks.getMap(i)
|
||||
if (earliest >= chunk.getDouble("earliest") || latest <= chunk.getDouble("latest")) {
|
||||
return chunk
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun insertPostInChannel(db: Database, channelId: String, earliest: Double, latest: Double): ReadableMap {
|
||||
val id = RandomId.generate()
|
||||
db.execute("insert into PostsInChannel (id, channel_id, earliest, latest, _status) values (?, ?, ?, ?, 'created')",
|
||||
arrayOf(id, channelId, earliest, latest))
|
||||
|
||||
val map = Arguments.createMap()
|
||||
map.putString("id", id)
|
||||
map.putString("channel_id", channelId)
|
||||
map.putDouble("earliest", earliest)
|
||||
map.putDouble("latest", latest)
|
||||
return map
|
||||
}
|
||||
|
||||
private fun mergePostsInChannel(db: Database, existingChunks: ReadableArray, newChunk: ReadableMap) {
|
||||
for (i in 0 until existingChunks.size()) {
|
||||
val chunk = existingChunks.getMap(i)
|
||||
if (newChunk.getDouble("earliest") <= chunk.getDouble("earliest") &&
|
||||
newChunk.getDouble("latest") >= chunk.getDouble("latest")) {
|
||||
db.execute("delete from PostsInChannel where id = ?", arrayOf(chunk.getString("id")))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePostsInThread(db: Database, postsInThread: Map<String, List<JSONObject>>) {
|
||||
postsInThread.forEach { (key, list) ->
|
||||
val sorted = list.sortedBy { it.getDouble("create_at") }
|
||||
val earliest = sorted.first().getDouble("create_at")
|
||||
val latest = sorted.last().getDouble("create_at")
|
||||
db.rawQuery("select * from PostsInThread where root_id = ? order by latest desc", arrayOf(key)).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
cursor.moveToFirst()
|
||||
val cursorMap = Arguments.createMap()
|
||||
cursorMap.mapCursor(cursor)
|
||||
db.execute(
|
||||
"update PostsInThread set earliest = ?, latest = ?, _status = 'updated' where id = ?",
|
||||
arrayOf(
|
||||
minOf(earliest, cursorMap.getDouble("earliest")),
|
||||
maxOf(latest, cursorMap.getDouble("latest")),
|
||||
key
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"insert into PostsInThread (id, root_id, earliest, latest, _status) " +
|
||||
"values (?, ?, ?, ?, 'created')",
|
||||
arrayOf(id, key, earliest, latest)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.toMap(): Map<String, *> = keys().asSequence().associateWith { it ->
|
||||
when (val value = this[it])
|
||||
{
|
||||
is JSONArray ->
|
||||
@@ -54,15 +683,9 @@ class DatabaseHelper {
|
||||
val map = (0 until value.length()).associate { Pair(it.toString(), value[it]) }
|
||||
JSONObject(map).toMap().values.toList()
|
||||
}
|
||||
is JSONObject -> {
|
||||
value.toMap()
|
||||
}
|
||||
JSONObject.NULL -> {
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
value
|
||||
}
|
||||
is JSONObject -> value.toMap()
|
||||
JSONObject.NULL -> null
|
||||
else -> value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,15 +700,3 @@ class DatabaseHelper {
|
||||
private set
|
||||
}
|
||||
}
|
||||
|
||||
fun WritableMap.mapCursor(cursor: Cursor) {
|
||||
for (i in 0 until cursor.columnCount) {
|
||||
when (cursor.getType(i)) {
|
||||
Cursor.FIELD_TYPE_NULL -> putNull(cursor.getColumnName(i))
|
||||
Cursor.FIELD_TYPE_INTEGER -> putDouble(cursor.getColumnName(i), cursor.getDouble(i))
|
||||
Cursor.FIELD_TYPE_FLOAT -> putDouble(cursor.getColumnName(i), cursor.getDouble(i))
|
||||
Cursor.FIELD_TYPE_STRING -> putString(cursor.getColumnName(i), cursor.getString(i))
|
||||
else -> putString(cursor.getColumnName(i), "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,26 +8,22 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
|
||||
import com.mattermost.networkclient.ApiClientModuleImpl;
|
||||
import com.mattermost.networkclient.APIClientModule;
|
||||
import com.mattermost.networkclient.enums.RetryTypes;
|
||||
import com.mattermost.turbolog.TurboLog;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.Response;
|
||||
|
||||
|
||||
public class Network {
|
||||
private static ApiClientModuleImpl clientModule;
|
||||
private static APIClientModule clientModule;
|
||||
private static final WritableMap clientOptions = Arguments.createMap();
|
||||
private static final Promise emptyPromise = new ResolvePromise();
|
||||
|
||||
public static void init(Context context) {
|
||||
if (clientModule == null) {
|
||||
clientModule = new ApiClientModuleImpl(context);
|
||||
createClientOptions();
|
||||
} else {
|
||||
TurboLog.Companion.i("ReactNative", "Network already initialized");
|
||||
}
|
||||
final ReactApplicationContext reactContext = new ReactApplicationContext(context);
|
||||
clientModule = new APIClientModule(reactContext);
|
||||
createClientOptions();
|
||||
}
|
||||
|
||||
public static void get(String baseUrl, String endpoint, ReadableMap options, Promise promise) {
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
package com.mattermost.helpers;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public class NotificationHelper {
|
||||
public static final String PUSH_NOTIFICATIONS = "PUSH_NOTIFICATIONS";
|
||||
public static final String NOTIFICATIONS_IN_GROUP = "notificationsInGroup";
|
||||
private static final String VERSION_PREFERENCE = "VERSION_PREFERENCE";
|
||||
|
||||
public static void cleanNotificationPreferencesIfNeeded(Context 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, JSONObject> inputMap = new HashMap<>();
|
||||
saveMap(context, inputMap);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static int getNotificationId(Bundle notification) {
|
||||
final String postId = notification.getString("post_id");
|
||||
final String channelId = notification.getString("channel_id");
|
||||
|
||||
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
|
||||
if (postId != null) {
|
||||
notificationId = postId.hashCode();
|
||||
} else if (channelId != null) {
|
||||
notificationId = channelId.hashCode();
|
||||
}
|
||||
|
||||
return notificationId;
|
||||
}
|
||||
|
||||
public static StatusBarNotification[] getDeliveredNotifications(Context context) {
|
||||
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
return notificationManager.getActiveNotifications();
|
||||
}
|
||||
|
||||
public static boolean addNotificationToPreferences(Context context, int notificationId, Bundle notification) {
|
||||
try {
|
||||
boolean createSummary = true;
|
||||
final String serverUrl = notification.getString("server_url");
|
||||
final String channelId = notification.getString("channel_id");
|
||||
final String rootId = notification.getString("root_id");
|
||||
final boolean isCRTEnabled = notification.containsKey("is_crt_enabled") && notification.getString("is_crt_enabled").equals("true");
|
||||
|
||||
final boolean isThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
final String groupId = isThreadNotification ? rootId : channelId;
|
||||
|
||||
Map<String, JSONObject> notificationsPerServer = loadMap(context);
|
||||
JSONObject notificationsInServer = notificationsPerServer.get(serverUrl);
|
||||
if (notificationsInServer == null) {
|
||||
notificationsInServer = new JSONObject();
|
||||
}
|
||||
|
||||
JSONObject notificationsInGroup = notificationsInServer.optJSONObject(groupId);
|
||||
if (notificationsInGroup == null) {
|
||||
notificationsInGroup = new JSONObject();
|
||||
}
|
||||
|
||||
if (notificationsInGroup.length() > 0) {
|
||||
createSummary = false;
|
||||
}
|
||||
|
||||
notificationsInGroup.put(String.valueOf(notificationId), false);
|
||||
|
||||
if (createSummary) {
|
||||
// Add the summary notification id as well
|
||||
notificationsInGroup.put(String.valueOf(notificationId + 1), true);
|
||||
}
|
||||
notificationsInServer.put(groupId, notificationsInGroup);
|
||||
notificationsPerServer.put(serverUrl, notificationsInServer);
|
||||
saveMap(context, notificationsPerServer);
|
||||
|
||||
return createSummary;
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void dismissNotification(Context context, Bundle notification) {
|
||||
final boolean isCRTEnabled = notification.containsKey("is_crt_enabled") && notification.getString("is_crt_enabled").equals("true");
|
||||
final String serverUrl = notification.getString("server_url");
|
||||
final String channelId = notification.getString("channel_id");
|
||||
final String rootId = notification.getString("root_id");
|
||||
|
||||
int notificationId = getNotificationId(notification);
|
||||
|
||||
if (!android.text.TextUtils.isEmpty(serverUrl) && !android.text.TextUtils.isEmpty(channelId)) {
|
||||
boolean isThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
String notificationIdStr = String.valueOf(notificationId);
|
||||
String groupId = isThreadNotification ? rootId : channelId;
|
||||
|
||||
Map<String, JSONObject> notificationsPerServer = loadMap(context);
|
||||
JSONObject notificationsInServer = notificationsPerServer.get(serverUrl);
|
||||
if (notificationsInServer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
JSONObject notificationsInGroup = notificationsInServer.optJSONObject(groupId);
|
||||
if (notificationsInGroup == null) {
|
||||
return;
|
||||
}
|
||||
boolean isSummary = notificationsInGroup.optBoolean(notificationIdStr);
|
||||
notificationsInGroup.remove(notificationIdStr);
|
||||
|
||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
notificationManager.cancel(notificationId);
|
||||
StatusBarNotification[] statusNotifications = getDeliveredNotifications(context);
|
||||
boolean hasMore = false;
|
||||
|
||||
for (final StatusBarNotification status : statusNotifications) {
|
||||
Bundle bundle = status.getNotification().extras;
|
||||
if (isThreadNotification) {
|
||||
hasMore = bundle.containsKey("root_id") && bundle.getString("root_id").equals(rootId);
|
||||
} else {
|
||||
hasMore = bundle.containsKey("channel_id") && bundle.getString("channel_id").equals(channelId);
|
||||
}
|
||||
if (hasMore) break;
|
||||
}
|
||||
|
||||
if (!hasMore || isSummary) {
|
||||
notificationsInServer.remove(groupId);
|
||||
} else {
|
||||
try {
|
||||
notificationsInServer.put(groupId, notificationsInGroup);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
notificationsPerServer.put(serverUrl, notificationsInServer);
|
||||
saveMap(context, notificationsPerServer);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeChannelNotifications(Context context, String serverUrl, String channelId) {
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
Map<String, JSONObject> notificationsPerServer = loadMap(context);
|
||||
JSONObject notificationsInServer = notificationsPerServer.get(serverUrl);
|
||||
|
||||
if (notificationsInServer != null) {
|
||||
notificationsInServer.remove(channelId);
|
||||
notificationsPerServer.put(serverUrl, notificationsInServer);
|
||||
saveMap(context, notificationsPerServer);
|
||||
}
|
||||
|
||||
StatusBarNotification[] notifications = getDeliveredNotifications(context);
|
||||
for (StatusBarNotification sbn:notifications) {
|
||||
Notification n = sbn.getNotification();
|
||||
Bundle bundle = n.extras;
|
||||
String cId = bundle.getString("channel_id");
|
||||
String rootId = bundle.getString("root_id");
|
||||
boolean isCRTEnabled = bundle.containsKey("is_crt_enabled") && bundle.getString("is_crt_enabled").equals("true");
|
||||
boolean skipThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
if (Objects.equals(cId, channelId) && !skipThreadNotification) {
|
||||
notificationManager.cancel(sbn.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeThreadNotifications(Context context, String serverUrl, String threadId) {
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
Map<String, JSONObject> notificationsPerServer = loadMap(context);
|
||||
JSONObject notificationsInServer = notificationsPerServer.get(serverUrl);
|
||||
|
||||
StatusBarNotification[] notifications = getDeliveredNotifications(context);
|
||||
for (StatusBarNotification sbn:notifications) {
|
||||
Notification n = sbn.getNotification();
|
||||
Bundle bundle = n.extras;
|
||||
String rootId = bundle.getString("root_id");
|
||||
String postId = bundle.getString("post_id");
|
||||
if (Objects.equals(rootId, threadId)) {
|
||||
notificationManager.cancel(sbn.getId());
|
||||
}
|
||||
|
||||
if (Objects.equals(postId, threadId)) {
|
||||
String channelId = bundle.getString("channel_id");
|
||||
int id = sbn.getId();
|
||||
if (notificationsInServer != null && channelId != null) {
|
||||
JSONObject notificationsInChannel = notificationsInServer.optJSONObject(channelId);
|
||||
if (notificationsInChannel != null) {
|
||||
notificationsInChannel.remove(String.valueOf(id));
|
||||
try {
|
||||
notificationsInServer.put(channelId, notificationsInChannel);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
notificationManager.cancel(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (notificationsInServer != null) {
|
||||
notificationsInServer.remove(threadId);
|
||||
notificationsPerServer.put(serverUrl, notificationsInServer);
|
||||
saveMap(context, notificationsPerServer);
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeServerNotifications(Context context, String serverUrl) {
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
Map<String, JSONObject> notificationsPerServer = loadMap(context);
|
||||
notificationsPerServer.remove(serverUrl);
|
||||
saveMap(context, notificationsPerServer);
|
||||
StatusBarNotification[] notifications = getDeliveredNotifications(context);
|
||||
for (StatusBarNotification sbn:notifications) {
|
||||
Notification n = sbn.getNotification();
|
||||
Bundle bundle = n.extras;
|
||||
String url = bundle.getString("server_url");
|
||||
if (Objects.equals(url, serverUrl)) {
|
||||
notificationManager.cancel(sbn.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearChannelOrThreadNotifications(Context context, Bundle notification) {
|
||||
final String serverUrl = notification.getString("server_url");
|
||||
final String channelId = notification.getString("channel_id");
|
||||
final String rootId = notification.getString("root_id");
|
||||
if (channelId != null) {
|
||||
final boolean isCRTEnabled = notification.containsKey("is_crt_enabled") && notification.getString("is_crt_enabled").equals("true");
|
||||
// rootId is available only when CRT is enabled & clearing the thread
|
||||
final boolean isClearThread = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
|
||||
if (isClearThread) {
|
||||
removeThreadNotifications(context, serverUrl, rootId);
|
||||
} else {
|
||||
removeChannelNotifications(context, serverUrl, channelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Map Structure
|
||||
*
|
||||
* { serverUrl: { groupId: { notification1: true, notification2: false } } }
|
||||
* summary notification has a value of true
|
||||
*
|
||||
*/
|
||||
|
||||
private static void saveMap(Context context, Map<String, JSONObject> inputMap) {
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
|
||||
if (pSharedPref != null) {
|
||||
JSONObject json = new JSONObject(inputMap);
|
||||
String jsonString = json.toString();
|
||||
SharedPreferences.Editor editor = pSharedPref.edit();
|
||||
editor.remove(NOTIFICATIONS_IN_GROUP).apply();
|
||||
editor.putString(NOTIFICATIONS_IN_GROUP, jsonString);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, JSONObject> loadMap(Context context) {
|
||||
Map<String, JSONObject> 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_GROUP, (new JSONObject()).toString());
|
||||
JSONObject json = new JSONObject(jsonString);
|
||||
Iterator<String> servers = json.keys();
|
||||
|
||||
while (servers.hasNext()) {
|
||||
String serverUrl = servers.next();
|
||||
JSONObject notificationGroup = json.getJSONObject(serverUrl);
|
||||
outputMap.put(serverUrl, notificationGroup);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return outputMap;
|
||||
}
|
||||
}
|
||||
@@ -2,161 +2,294 @@ package com.mattermost.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.database_extension.getDatabaseForServer
|
||||
import com.mattermost.helpers.database_extension.saveToDatabase
|
||||
import com.mattermost.helpers.push_notification.addToDefaultCategoryIfNeeded
|
||||
import com.mattermost.helpers.push_notification.fetchMyChannel
|
||||
import com.mattermost.helpers.push_notification.fetchMyTeamCategories
|
||||
import com.mattermost.helpers.push_notification.fetchNeededUsers
|
||||
import com.mattermost.helpers.push_notification.fetchPosts
|
||||
import com.mattermost.helpers.push_notification.fetchTeamIfNeeded
|
||||
import com.mattermost.helpers.push_notification.fetchThread
|
||||
import com.mattermost.turbolog.TurboLog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.facebook.react.bridge.WritableNativeArray
|
||||
import com.nozbe.watermelondb.Database
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.*
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class PushNotificationDataHelper(private val context: Context) {
|
||||
suspend fun fetchAndStoreDataForPushNotification(initialData: Bundle, isReactInit: Boolean): Bundle? {
|
||||
return withContext(Dispatchers.Default) {
|
||||
PushNotificationDataRunnable.start(context, initialData, isReactInit)
|
||||
}
|
||||
private var scope = Executors.newSingleThreadExecutor()
|
||||
fun fetchAndStoreDataForPushNotification(initialData: Bundle) {
|
||||
scope.execute(Runnable {
|
||||
runBlocking {
|
||||
PushNotificationDataRunnable.start(context, initialData)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class PushNotificationDataRunnable {
|
||||
companion object {
|
||||
internal val specialMentions = listOf("all", "here", "channel")
|
||||
private val dbHelper = DatabaseHelper.instance!!
|
||||
private val mutex = Mutex()
|
||||
private val specialMentions = listOf("all", "here", "channel")
|
||||
|
||||
suspend fun start(context: Context, initialData: Bundle, isReactInit: Boolean): Bundle? {
|
||||
mutex.withLock {
|
||||
// for more info see: https://blog.danlew.net/2020/01/28/coroutines-and-java-synchronization-dont-mix/
|
||||
val serverUrl: String = initialData.getString("server_url") ?: return null
|
||||
val db = dbHelper.getDatabaseForServer(context, serverUrl)
|
||||
var result: Bundle? = null
|
||||
@Synchronized
|
||||
suspend fun start(context: Context, initialData: Bundle) {
|
||||
try {
|
||||
val serverUrl: String = initialData.getString("server_url") ?: return
|
||||
val channelId = initialData.getString("channel_id")
|
||||
val rootId = initialData.getString("root_id")
|
||||
val isCRTEnabled = initialData.getString("is_crt_enabled") == "true"
|
||||
val db = DatabaseHelper.instance!!.getDatabaseForServer(context, serverUrl)
|
||||
Log.i("ReactNative", "Start fetching notification data in server="+serverUrl+" for channel="+channelId)
|
||||
|
||||
try {
|
||||
if (db != null) {
|
||||
val teamId = initialData.getString("team_id")
|
||||
val channelId = initialData.getString("channel_id")
|
||||
val postId = initialData.getString("post_id")
|
||||
val rootId = initialData.getString("root_id")
|
||||
val isCRTEnabled = initialData.getString("is_crt_enabled") == "true"
|
||||
val ackId = initialData.getString("ack_id")
|
||||
if (db != null) {
|
||||
var postData: ReadableMap?
|
||||
var posts: ReadableMap? = null
|
||||
var userIdsToLoad: ReadableArray? = null
|
||||
var usernamesToLoad: ReadableArray? = null
|
||||
|
||||
TurboLog.i("ReactNative", "Start fetching notification data in server=$serverUrl for channel=$channelId and ack=$ackId")
|
||||
var threads: ReadableArray? = null
|
||||
var usersFromThreads: ReadableArray? = null
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
val notificationData = Arguments.createMap()
|
||||
coroutineScope {
|
||||
if (channelId != null) {
|
||||
postData = fetchPosts(db, serverUrl, channelId, isCRTEnabled, rootId)
|
||||
|
||||
var channel: ReadableMap? = null
|
||||
var myTeam: ReadableMap? = null
|
||||
posts = postData?.getMap("posts")
|
||||
userIdsToLoad = postData?.getArray("userIdsToLoad")
|
||||
usernamesToLoad = postData?.getArray("usernamesToLoad")
|
||||
threads = postData?.getArray("threads")
|
||||
usersFromThreads = postData?.getArray("usersFromThreads")
|
||||
|
||||
if (!teamId.isNullOrEmpty()) {
|
||||
val res = fetchTeamIfNeeded(db, serverUrl, teamId)
|
||||
res.first?.let { notificationData.putMap("team", it) }
|
||||
|
||||
myTeam = res.second
|
||||
myTeam?.let { notificationData.putMap("myTeam", it) }
|
||||
}
|
||||
|
||||
if (channelId != null && postId != null) {
|
||||
val channelRes = fetchMyChannel(db, serverUrl, channelId, isCRTEnabled)
|
||||
|
||||
channel = channelRes.first
|
||||
channel?.let { notificationData.putMap("channel", it) }
|
||||
channelRes.second?.let { notificationData.putMap("myChannel", it) }
|
||||
val loadedProfiles = channelRes.third
|
||||
|
||||
// Fetch categories if needed
|
||||
if (!teamId.isNullOrEmpty() && myTeam != null) {
|
||||
// should load all categories
|
||||
val res = fetchMyTeamCategories(db, serverUrl, teamId)
|
||||
res?.let { notificationData.putMap("categories", it) }
|
||||
} else if (channel != null) {
|
||||
// check if the channel is in the category for the team
|
||||
val res = addToDefaultCategoryIfNeeded(db, channel)
|
||||
res?.let { notificationData.putArray("categoryChannels", it) }
|
||||
if (userIdsToLoad != null && userIdsToLoad!!.size() > 0) {
|
||||
val users = fetchUsersById(serverUrl, userIdsToLoad!!)
|
||||
userIdsToLoad = users?.getArray("data")
|
||||
}
|
||||
|
||||
val postData = fetchPosts(db, serverUrl, channelId, isCRTEnabled, rootId, loadedProfiles)
|
||||
postData?.getMap("posts")?.let { notificationData.putMap("posts", it) }
|
||||
|
||||
var notificationThread: ReadableMap? = null
|
||||
if (isCRTEnabled && !rootId.isNullOrEmpty()) {
|
||||
notificationThread = fetchThread(db, serverUrl, rootId, teamId)
|
||||
if (usernamesToLoad != null && usernamesToLoad!!.size() > 0) {
|
||||
val users = fetchUsersByUsernames(serverUrl, usernamesToLoad!!)
|
||||
usernamesToLoad = users?.getArray("data")
|
||||
}
|
||||
|
||||
getThreadList(notificationThread, postData?.getArray("threads"))?.let {
|
||||
val threadsArray = Arguments.createArray()
|
||||
for (item in it) {
|
||||
threadsArray.pushMap(item)
|
||||
}
|
||||
notificationData.putArray("threads", threadsArray)
|
||||
}
|
||||
|
||||
val userList = fetchNeededUsers(serverUrl, loadedProfiles, postData)
|
||||
notificationData.putArray("users", ReadableArrayUtils.toWritableArray(userList.toArray()))
|
||||
}
|
||||
|
||||
result = Arguments.toBundle(notificationData)
|
||||
|
||||
if (!isReactInit) {
|
||||
dbHelper.saveToDatabase(db, notificationData, teamId, channelId, receivingThreads)
|
||||
}
|
||||
|
||||
TurboLog.i("ReactNative", "Done processing push notification=$serverUrl for channel=$channelId and ack=$ackId")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
val eMessage = e.message ?: "Error with no message"
|
||||
TurboLog.e("ReactNative", "Error processing push notification error=$eMessage")
|
||||
} finally {
|
||||
db?.close()
|
||||
TurboLog.i("ReactNative", "DONE fetching notification data")
|
||||
}
|
||||
|
||||
return result
|
||||
db.transaction {
|
||||
if (posts != null && channelId != null) {
|
||||
DatabaseHelper.instance!!.handlePosts(db, posts!!.getMap("data"), channelId, receivingThreads)
|
||||
}
|
||||
|
||||
if (threads != null) {
|
||||
DatabaseHelper.instance!!.handleThreads(db, threads!!)
|
||||
}
|
||||
|
||||
if (userIdsToLoad != null && userIdsToLoad!!.size() > 0) {
|
||||
DatabaseHelper.instance!!.handleUsers(db, userIdsToLoad!!)
|
||||
}
|
||||
|
||||
if (usernamesToLoad != null && usernamesToLoad!!.size() > 0) {
|
||||
DatabaseHelper.instance!!.handleUsers(db, usernamesToLoad!!)
|
||||
}
|
||||
|
||||
if (usersFromThreads != null) {
|
||||
DatabaseHelper.instance!!.handleUsers(db, usersFromThreads!!)
|
||||
}
|
||||
}
|
||||
|
||||
db.close()
|
||||
Log.i("ReactNative", "Done processing push notification="+serverUrl+" for channel="+channelId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getThreadList(notificationThread: ReadableMap?, threads: ReadableArray?): ArrayList<ReadableMap>? {
|
||||
threads?.let {
|
||||
val threadsArray = ArrayList<ReadableMap>()
|
||||
val threadIds = ArrayList<String>()
|
||||
notificationThread?.let { thread ->
|
||||
thread.getString("id")?.let { it1 -> threadIds.add(it1) }
|
||||
threadsArray.add(thread)
|
||||
}
|
||||
for(i in 0 until it.size()) {
|
||||
val thread = it.getMap(i)
|
||||
val threadId = thread.getString("id")
|
||||
if (threadId != null) {
|
||||
if (threadIds.contains(threadId)) {
|
||||
// replace the values for participants and is_following
|
||||
val index = threadsArray.indexOfFirst { el -> el.getString("id") == threadId }
|
||||
val prev = threadsArray[index]
|
||||
val merge = Arguments.createMap()
|
||||
merge.merge(prev)
|
||||
merge.putBoolean("is_following", thread.getBoolean("is_following"))
|
||||
merge.putArray("participants", thread.getArray("participants"))
|
||||
threadsArray[index] = merge
|
||||
} else {
|
||||
threadsArray.add(thread)
|
||||
threadIds.add(threadId)
|
||||
private suspend fun fetchPosts(db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean, rootId: String?): ReadableMap? {
|
||||
val regex = Regex("""\B@(([a-z0-9-._]*[a-z0-9_])[.-]*)""", setOf(RegexOption.IGNORE_CASE))
|
||||
val since = DatabaseHelper.instance!!.queryPostSinceForChannel(db, channelId)
|
||||
val currentUserId = DatabaseHelper.instance!!.queryCurrentUserId(db)?.removeSurrounding("\"")
|
||||
val currentUser = DatabaseHelper.instance!!.find(db, "User", currentUserId)
|
||||
val currentUsername = currentUser?.getString("username")
|
||||
|
||||
var additionalParams = ""
|
||||
if (isCRTEnabled) {
|
||||
additionalParams = "&collapsedThreads=true&collapsedThreadsExtended=true"
|
||||
}
|
||||
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
val endpoint = if (receivingThreads) {
|
||||
val queryParams = "?skipFetchThreads=false&perPage=60&fromCreatedAt=0&direction=up"
|
||||
"/api/v4/posts/$rootId/thread$queryParams$additionalParams"
|
||||
} else {
|
||||
val queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}"
|
||||
"/api/v4/channels/$channelId/posts$queryParams$additionalParams"
|
||||
}
|
||||
|
||||
val postsResponse = fetch(serverUrl, endpoint)
|
||||
val results = Arguments.createMap()
|
||||
|
||||
if (postsResponse != null) {
|
||||
val data = ReadableMapUtils.toMap(postsResponse)
|
||||
results.putMap("posts", postsResponse)
|
||||
val postsData = data["data"] as? Map<*, *>
|
||||
if (postsData != null) {
|
||||
val postsMap = postsData["posts"]
|
||||
if (postsMap != null) {
|
||||
val posts = ReadableMapUtils.toWritableMap(postsMap as? Map<String, Any>)
|
||||
val iterator = posts.keySetIterator()
|
||||
val userIds = mutableListOf<String>()
|
||||
val usernames = mutableListOf<String>()
|
||||
|
||||
val threads = WritableNativeArray()
|
||||
val threadParticipantUserIds = mutableListOf<String>() // Used to exclude the "userIds" present in the thread participants
|
||||
val threadParticipantUsernames = mutableListOf<String>() // Used to exclude the "usernames" present in the thread participants
|
||||
val threadParticipantUsers = HashMap<String, ReadableMap>() // All unique users from thread participants are stored here
|
||||
|
||||
while(iterator.hasNextKey()) {
|
||||
val key = iterator.nextKey()
|
||||
val post = posts.getMap(key)
|
||||
val userId = post?.getString("user_id")
|
||||
if (userId != null && userId != currentUserId && !userIds.contains(userId)) {
|
||||
userIds.add(userId)
|
||||
}
|
||||
val message = post?.getString("message")
|
||||
if (message != null) {
|
||||
val matchResults = regex.findAll(message)
|
||||
matchResults.iterator().forEach {
|
||||
val username = it.value.removePrefix("@")
|
||||
if (!usernames.contains(username) && currentUsername != username && !specialMentions.contains(username)) {
|
||||
usernames.add(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isCRTEnabled) {
|
||||
// Add root post as a thread
|
||||
val threadId = post?.getString("root_id")
|
||||
if (threadId.isNullOrEmpty()) {
|
||||
threads.pushMap(post!!)
|
||||
}
|
||||
|
||||
// Add participant userIds and usernames to exclude them from getting fetched again
|
||||
val participants = post.getArray("participants")
|
||||
if (participants != null) {
|
||||
for (i in 0 until participants.size()) {
|
||||
val participant = participants.getMap(i)
|
||||
|
||||
val participantId = participant.getString("id")
|
||||
if (participantId != currentUserId && participantId != null) {
|
||||
if (!threadParticipantUserIds.contains(participantId)) {
|
||||
threadParticipantUserIds.add(participantId)
|
||||
}
|
||||
|
||||
if (!threadParticipantUsers.containsKey(participantId)) {
|
||||
threadParticipantUsers[participantId] = participant
|
||||
}
|
||||
}
|
||||
|
||||
val username = participant.getString("username")
|
||||
if (username != null && username != currentUsername && !threadParticipantUsernames.contains(username)) {
|
||||
threadParticipantUsernames.add(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val existingUserIds = DatabaseHelper.instance!!.queryIds(db, "User", userIds.toTypedArray())
|
||||
val existingUsernames = DatabaseHelper.instance!!.queryByColumn(db, "User", "username", usernames.toTypedArray())
|
||||
userIds.removeAll { it in existingUserIds }
|
||||
usernames.removeAll { it in existingUsernames }
|
||||
|
||||
if (threadParticipantUserIds.size > 0) {
|
||||
// Do not fetch users found in thread participants as we get the user's data in the posts response already
|
||||
userIds.removeAll { it in threadParticipantUserIds }
|
||||
usernames.removeAll { it in threadParticipantUsernames }
|
||||
|
||||
// Get users from thread participants
|
||||
val existingThreadParticipantUserIds = DatabaseHelper.instance!!.queryIds(db, "User", threadParticipantUserIds.toTypedArray())
|
||||
|
||||
// Exclude the thread participants already present in the DB from getting inserted again
|
||||
val usersFromThreads = WritableNativeArray()
|
||||
threadParticipantUsers.forEach{ (userId, user) ->
|
||||
if (!existingThreadParticipantUserIds.contains(userId)) {
|
||||
usersFromThreads.pushMap(user)
|
||||
}
|
||||
}
|
||||
|
||||
if (usersFromThreads.size() > 0) {
|
||||
results.putArray("usersFromThreads", usersFromThreads)
|
||||
}
|
||||
}
|
||||
|
||||
if (userIds.size > 0) {
|
||||
results.putArray("userIdsToLoad", ReadableArrayUtils.toWritableArray(userIds.toTypedArray()))
|
||||
}
|
||||
|
||||
if (usernames.size > 0) {
|
||||
results.putArray("usernamesToLoad", ReadableArrayUtils.toWritableArray(usernames.toTypedArray()))
|
||||
}
|
||||
|
||||
if (threads.size() > 0) {
|
||||
results.putArray("threads", threads)
|
||||
}
|
||||
}
|
||||
}
|
||||
return threadsArray
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
return null
|
||||
private suspend fun fetchUsersById(serverUrl: String, userIds: ReadableArray): ReadableMap? {
|
||||
val endpoint = "api/v4/users/ids"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(userIds)))
|
||||
return fetchWithPost(serverUrl, endpoint, options)
|
||||
}
|
||||
|
||||
private suspend fun fetchUsersByUsernames(serverUrl: String, usernames: ReadableArray): ReadableMap? {
|
||||
val endpoint = "api/v4/users/usernames"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(usernames)))
|
||||
return fetchWithPost(serverUrl, endpoint, options)
|
||||
}
|
||||
|
||||
private suspend fun fetch(serverUrl: String, endpoint: String): ReadableMap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Network.get(serverUrl, endpoint, null, object : ResolvePromise() {
|
||||
override fun resolve(value: Any?) {
|
||||
val response = value as ReadableMap?
|
||||
if (response != null && !response.getBoolean("ok")) {
|
||||
val error = response.getMap("data")
|
||||
cont.resumeWith(Result.failure((IOException("Unexpected code ${error?.getInt("status_code")} ${error?.getString("message")}"))))
|
||||
} else {
|
||||
cont.resumeWith(Result.success(response))
|
||||
}
|
||||
}
|
||||
|
||||
override fun reject(code: String, message: String) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
|
||||
}
|
||||
|
||||
override fun reject(reason: Throwable?) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchWithPost(serverUrl: String, endpoint: String, options: ReadableMap?) : ReadableMap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Network.post(serverUrl, endpoint, options, object : ResolvePromise() {
|
||||
override fun resolve(value: Any?) {
|
||||
val response = value as ReadableMap?
|
||||
cont.resumeWith(Result.success(response))
|
||||
}
|
||||
|
||||
override fun reject(code: String, message: String) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
|
||||
}
|
||||
|
||||
override fun reject(reason: Throwable?) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
package com.mattermost.helpers
|
||||
|
||||
import java.util.UUID
|
||||
import kotlin.math.floor
|
||||
|
||||
class RandomId {
|
||||
companion object {
|
||||
private const val alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
private const val alphabetLength = alphabet.length
|
||||
private const val idLength = 16
|
||||
|
||||
fun generate(): String {
|
||||
return UUID.randomUUID().toString()
|
||||
var id = ""
|
||||
for (i in 1.rangeTo((idLength / 2))) {
|
||||
val random = floor(Math.random() * alphabetLength * alphabetLength)
|
||||
id += alphabet[floor(random / alphabetLength).toInt()]
|
||||
id += alphabet[(random % alphabetLength).toInt()]
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.mattermost.helpers;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReadableType;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
|
||||
@@ -110,9 +109,7 @@ public class ReadableArrayUtils {
|
||||
writableArray.pushString((String) value);
|
||||
} else if (value instanceof Map) {
|
||||
writableArray.pushMap(ReadableMapUtils.toWritableMap((Map<String, Object>) value));
|
||||
} else if (value instanceof ReadableMap) {
|
||||
writableArray.pushMap((ReadableMap) value);
|
||||
}else if (value.getClass().isArray()) {
|
||||
} else if (value.getClass().isArray()) {
|
||||
writableArray.pushArray(ReadableArrayUtils.toWritableArray((Object[]) value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
package com.mattermost.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.content.ContentResolver;
|
||||
import android.os.Environment;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.util.Log;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
// Class based on DocumentHelper https://gist.github.com/steveevers/a5af24c226f44bb8fdc3
|
||||
|
||||
public class RealPathUtil {
|
||||
public static final String CACHE_DIR_NAME = "mmShare";
|
||||
public static String getRealPathFromURI(final Context context, final Uri uri) {
|
||||
|
||||
// DocumentProvider
|
||||
if (DocumentsContract.isDocumentUri(context, uri)) {
|
||||
// ExternalStorageProvider
|
||||
if (isExternalStorageDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
if ("primary".equalsIgnoreCase(type)) {
|
||||
return Environment.getExternalStorageDirectory() + "/" + split[1];
|
||||
}
|
||||
} else if (isDownloadsDocument(uri)) {
|
||||
// DownloadsProvider
|
||||
|
||||
final String id = DocumentsContract.getDocumentId(uri);
|
||||
if (!TextUtils.isEmpty(id)) {
|
||||
if (id.startsWith("raw:")) {
|
||||
return id.replaceFirst("raw:", "");
|
||||
}
|
||||
try {
|
||||
return getPathFromSavingTempFile(context, uri);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e("ReactNative", "DownloadsProvider unexpected uri " + uri);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else if (isMediaDocument(uri)) {
|
||||
// MediaProvider
|
||||
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
Uri contentUri = null;
|
||||
if ("image".equals(type)) {
|
||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("video".equals(type)) {
|
||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("audio".equals(type)) {
|
||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
}
|
||||
|
||||
final String selection = "_id=?";
|
||||
final String[] selectionArgs = new String[] {
|
||||
split[1]
|
||||
};
|
||||
|
||||
String name = getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
if (!TextUtils.isEmpty(name)) {
|
||||
return name;
|
||||
}
|
||||
|
||||
return getPathFromSavingTempFile(context, uri);
|
||||
}
|
||||
}
|
||||
|
||||
if ("content".equalsIgnoreCase(uri.getScheme())) {
|
||||
// MediaStore (and general)
|
||||
|
||||
if (isGooglePhotosUri(uri)) {
|
||||
return uri.getLastPathSegment();
|
||||
}
|
||||
|
||||
// Try save to tmp file, and return tmp file path
|
||||
return getPathFromSavingTempFile(context, uri);
|
||||
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getPathFromSavingTempFile(Context context, final Uri uri) {
|
||||
File tmpFile;
|
||||
String fileName = "";
|
||||
|
||||
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();
|
||||
} 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());
|
||||
}
|
||||
|
||||
|
||||
File cacheDir = new File(context.getCacheDir(), CACHE_DIR_NAME);
|
||||
if (!cacheDir.exists()) {
|
||||
cacheDir.mkdirs();
|
||||
}
|
||||
|
||||
tmpFile = new File(cacheDir, fileName);
|
||||
tmpFile.createNewFile();
|
||||
|
||||
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
|
||||
|
||||
FileChannel src = new FileInputStream(pfd.getFileDescriptor()).getChannel();
|
||||
FileChannel dst = new FileOutputStream(tmpFile).getChannel();
|
||||
dst.transferFrom(src, 0, src.size());
|
||||
src.close();
|
||||
dst.close();
|
||||
} catch (IOException ex) {
|
||||
return null;
|
||||
}
|
||||
return tmpFile.getAbsolutePath();
|
||||
}
|
||||
|
||||
public static String getDataColumn(Context context, Uri uri, String selection,
|
||||
String[] selectionArgs) {
|
||||
|
||||
Cursor cursor = null;
|
||||
final String column = "_data";
|
||||
final String[] projection = {
|
||||
column
|
||||
};
|
||||
|
||||
try {
|
||||
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
|
||||
null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
final int index = cursor.getColumnIndexOrThrow(column);
|
||||
return cursor.getString(index);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static boolean isExternalStorageDocument(Uri uri) {
|
||||
return "com.android.externalstorage.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
public static boolean isDownloadsDocument(Uri uri) {
|
||||
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
public static boolean isMediaDocument(Uri uri) {
|
||||
return "com.android.providers.media.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
public static boolean isGooglePhotosUri(Uri uri) {
|
||||
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
public static String getExtension(String uri) {
|
||||
String extension = "";
|
||||
if (uri == null) {
|
||||
return extension;
|
||||
}
|
||||
|
||||
extension = MimeTypeMap.getFileExtensionFromUrl(uri);
|
||||
if (!extension.equals("")) {
|
||||
return extension;
|
||||
}
|
||||
|
||||
int dot = uri.lastIndexOf(".");
|
||||
if (dot >= 0) {
|
||||
return uri.substring(dot);
|
||||
} else {
|
||||
// No extension.
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static String getMimeType(File file) {
|
||||
|
||||
String extension = getExtension(file.getName());
|
||||
|
||||
if (extension.length() > 0)
|
||||
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.substring(1));
|
||||
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
public static String getMimeType(String filePath) {
|
||||
File file = new File(filePath);
|
||||
return getMimeType(file);
|
||||
}
|
||||
|
||||
public static String getMimeTypeFromUri(final Context context, final Uri uri) {
|
||||
try {
|
||||
ContentResolver cR = context.getContentResolver();
|
||||
return cR.getType(uri);
|
||||
} catch (Exception e) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteTempFiles(final File dir) {
|
||||
try {
|
||||
if (dir.isDirectory()) {
|
||||
deleteRecursive(dir);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
private static void deleteRecursive(File fileOrDirectory) {
|
||||
if (fileOrDirectory.isDirectory()) {
|
||||
File[] files = fileOrDirectory.listFiles();
|
||||
if (files != null) {
|
||||
for (File child : files)
|
||||
deleteRecursive(child);
|
||||
}
|
||||
}
|
||||
|
||||
fileOrDirectory.delete();
|
||||
}
|
||||
|
||||
private static String sanitizeFilename(String filename) {
|
||||
if (filename == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
File f = new File(filename);
|
||||
return f.getName();
|
||||
}
|
||||
|
||||
public static File createDirIfNotExists(String path) {
|
||||
File dir = new File(path);
|
||||
if (dir.exists()) {
|
||||
return dir;
|
||||
}
|
||||
|
||||
try {
|
||||
dir.mkdirs();
|
||||
// Add .nomedia to hide the thumbnail directory from gallery
|
||||
File noMedia = new File(path, ".nomedia");
|
||||
noMedia.createNewFile();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.mattermost.helpers;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
@@ -10,59 +9,58 @@ import com.facebook.react.bridge.WritableMap;
|
||||
* ResolvePromise: Helper class that abstracts boilerplate
|
||||
*/
|
||||
public class ResolvePromise implements Promise {
|
||||
|
||||
@Override
|
||||
public void reject(@NonNull String s) {
|
||||
public void resolve(@javax.annotation.Nullable Object value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resolve(@Nullable Object o) {
|
||||
public void reject(String code, String message) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(@NonNull String s, @Nullable String s1) {
|
||||
public void reject(String code, @NonNull WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(@NonNull String s, @Nullable Throwable throwable) {
|
||||
public void reject(String code, Throwable e) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(@NonNull String s, @Nullable String s1, @Nullable Throwable throwable) {
|
||||
public void reject(Throwable e, WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(@NonNull Throwable throwable) {
|
||||
public void reject(String code, Throwable e, WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(@NonNull Throwable throwable, @NonNull WritableMap writableMap) {
|
||||
public void reject(String code, String message, Throwable e, WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(@NonNull String s, @NonNull WritableMap writableMap) {
|
||||
public void reject(String code, String message, Throwable e) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(@NonNull String s, @Nullable Throwable throwable, @NonNull WritableMap writableMap) {
|
||||
public void reject(String code, String message, @NonNull WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(@NonNull String s, @Nullable String s1, @NonNull WritableMap writableMap) {
|
||||
public void reject(String message) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(@Nullable String s, @Nullable String s1, @Nullable Throwable throwable, @Nullable WritableMap writableMap) {
|
||||
public void reject(Throwable reason) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
|
||||
fun insertCategory(db: WMDatabase, category: ReadableMap) {
|
||||
try {
|
||||
val id = category.getString("id") ?: return
|
||||
val collapsed = false
|
||||
val displayName = category.getString("display_name")
|
||||
val muted = category.getBoolean("muted")
|
||||
val sortOrder = category.getInt("sort_order")
|
||||
val sorting = category.getString("sorting") ?: "recent"
|
||||
val teamId = category.getString("team_id")
|
||||
val type = category.getString("type")
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Category
|
||||
(id, collapsed, display_name, muted, sort_order, sorting, team_id, type, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, collapsed, displayName, muted,
|
||||
sortOrder / 10, sorting, teamId, type
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertCategoryChannels(db: WMDatabase, categoryId: String, teamId: String, channelIds: ReadableArray) {
|
||||
try {
|
||||
for (i in 0 until channelIds.size()) {
|
||||
val channelId = channelIds.getString(i)
|
||||
val id = "${teamId}_$channelId"
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO CategoryChannel
|
||||
(id, category_id, channel_id, sort_order, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, categoryId, channelId, i)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertCategoriesWithChannels(db: WMDatabase, orderCategories: ReadableMap) {
|
||||
val categories = orderCategories.getArray("categories") ?: return
|
||||
for (i in 0 until categories.size()) {
|
||||
val category = categories.getMap(i)
|
||||
val id = category.getString("id")
|
||||
val teamId = category.getString("team_id")
|
||||
val channelIds = category.getArray("channel_ids")
|
||||
insertCategory(db, category)
|
||||
if (id != null && teamId != null) {
|
||||
channelIds?.let { insertCategoryChannels(db, id, teamId, it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun insertChannelToDefaultCategory(db: WMDatabase, categoryChannels: ReadableArray) {
|
||||
try {
|
||||
for (i in 0 until categoryChannels.size()) {
|
||||
val cc = categoryChannels.getMap(i)
|
||||
val id = cc.getString("id")
|
||||
val categoryId = cc.getString("category_id")
|
||||
val channelId = cc.getString("channel_id")
|
||||
val count = countByColumn(db, "CategoryChannel", "category_id", categoryId)
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO CategoryChannel
|
||||
(id, category_id, channel_id, sort_order, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, categoryId, channelId, if (count > 0) count + 1 else count)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.DatabaseHelper
|
||||
import com.mattermost.helpers.ReadableMapUtils
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
fun findChannel(db: WMDatabase?, channelId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "Channel", channelId)
|
||||
return team != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun findMyChannel(db: WMDatabase?, channelId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "MyChannel", channelId)
|
||||
return team != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
internal fun handleChannel(db: WMDatabase, channel: ReadableMap) {
|
||||
try {
|
||||
val exists = channel.getString("id")?.let { findChannel(db, it) } ?: false
|
||||
if (!exists) {
|
||||
val json = ReadableMapUtils.toJSONObject(channel)
|
||||
if (insertChannel(db, json)) {
|
||||
insertChannelInfo(db, json)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun DatabaseHelper.handleMyChannel(db: WMDatabase, myChannel: ReadableMap, postsData: ReadableMap?, receivingThreads: Boolean) {
|
||||
try {
|
||||
val json = ReadableMapUtils.toJSONObject(myChannel)
|
||||
val exists = myChannel.getString("id")?.let { findMyChannel(db, it) } ?: false
|
||||
|
||||
if (postsData != null && !receivingThreads) {
|
||||
val posts = ReadableMapUtils.toJSONObject(postsData.getMap("posts")).toMap()
|
||||
val postList = posts.toList()
|
||||
val lastFetchedAt = postList.fold(0.0) { acc, next ->
|
||||
val post = next.second as Map<*, *>
|
||||
val createAt = post["create_at"] as Double
|
||||
val updateAt = post["update_at"] as Double
|
||||
val deleteAt = post["delete_at"] as Double
|
||||
val value = maxOf(createAt, updateAt, deleteAt)
|
||||
|
||||
maxOf(value, acc)
|
||||
}
|
||||
json.put("last_fetched_at", lastFetchedAt)
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
updateMyChannel(db, json)
|
||||
return
|
||||
}
|
||||
|
||||
if (insertMyChannel(db, json)) {
|
||||
insertMyChannelSettings(db, json)
|
||||
insertChannelMember(db, json)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertChannel(db: WMDatabase, channel: JSONObject): Boolean {
|
||||
val id = try { channel.getString("id") } catch (e: JSONException) { return false }
|
||||
val createAt = try { channel.getDouble("create_at") } catch (e: JSONException) { 0 }
|
||||
val deleteAt = try { channel.getDouble("delete_at") } catch (e: JSONException) { 0 }
|
||||
val updateAt = try { channel.getDouble("update_at") } catch (e: JSONException) { 0 }
|
||||
val creatorId = try { channel.getString("creator_id") } catch (e: JSONException) { "" }
|
||||
val displayName = try { channel.getString("display_name") } catch (e: JSONException) { "" }
|
||||
val name = try { channel.getString("name") } catch (e: JSONException) { "" }
|
||||
val teamId = try { channel.getString("team_id") } catch (e: JSONException) { "" }
|
||||
val type = try { channel.getString("type") } catch (e: JSONException) { "O" }
|
||||
val isGroupConstrained = try { channel.getBoolean("group_constrained") } catch (e: JSONException) { false }
|
||||
val shared = try { channel.getBoolean("shared") } catch (e: JSONException) { false }
|
||||
|
||||
return try {
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Channel
|
||||
(id, create_at, delete_at, update_at, creator_id, display_name, name, team_id, type, is_group_constrained, shared, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, createAt, deleteAt, updateAt,
|
||||
creatorId, displayName, name, teamId, type,
|
||||
isGroupConstrained, shared
|
||||
)
|
||||
)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun insertChannelInfo(db: WMDatabase, channel: JSONObject) {
|
||||
val id = try { channel.getString("id") } catch (e: JSONException) { return }
|
||||
val header = try { channel.getString("header") } catch (e: JSONException) { "" }
|
||||
val purpose = try { channel.getString("purpose") } catch (e: JSONException) { "" }
|
||||
|
||||
try {
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ChannelInfo
|
||||
(id, header, purpose, guest_count, member_count, pinned_post_count, _changed, _status)
|
||||
VALUES (?, ?, ?, 0, 0, 0, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, header, purpose)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertMyChannel(db: WMDatabase, myChanel: JSONObject): Boolean {
|
||||
return try {
|
||||
val id = try { myChanel.getString("id") } catch (e: JSONException) { return false }
|
||||
val roles = try { myChanel.getString("roles") } catch (e: JSONException) { "" }
|
||||
val msgCount = try { myChanel.getInt("message_count") } catch (e: JSONException) { 0 }
|
||||
val mentionsCount = try { myChanel.getInt("mentions_count") } catch (e: JSONException) { 0 }
|
||||
val isUnread = try { myChanel.getBoolean("is_unread") } catch (e: JSONException) { false }
|
||||
val lastPostAt = try { myChanel.getDouble("last_post_at") } catch (e: JSONException) { 0 }
|
||||
val lastViewedAt = try { myChanel.getDouble("last_viewed_at") } catch (e: JSONException) { 0 }
|
||||
val viewedAt = 0
|
||||
val lastFetchedAt = try { myChanel.getDouble("last_fetched_at") } catch (e: JSONException) { 0 }
|
||||
val manuallyUnread = false
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO MyChannel
|
||||
(id, roles, message_count, mentions_count, is_unread, manually_unread,
|
||||
last_post_at, last_viewed_at, viewed_at, last_fetched_at, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""",
|
||||
arrayOf(
|
||||
id, roles, msgCount, mentionsCount, isUnread, manuallyUnread,
|
||||
lastPostAt, lastViewedAt, viewedAt, lastFetchedAt
|
||||
)
|
||||
)
|
||||
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun insertMyChannelSettings(db: WMDatabase, myChanel: JSONObject) {
|
||||
try {
|
||||
val id = try { myChanel.getString("id") } catch (e: JSONException) { return }
|
||||
val notifyProps = try { myChanel.getString("notify_props") } catch (e: JSONException) { return }
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO MyChannelSettings (id, notify_props, _changed, _status)
|
||||
VALUES (?, ?, '', 'created')
|
||||
""",
|
||||
arrayOf(id, notifyProps)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertChannelMember(db: WMDatabase, myChanel: JSONObject) {
|
||||
try {
|
||||
val userId = queryCurrentUserId(db) ?: return
|
||||
val channelId = try { myChanel.getString("id") } catch (e: JSONException) { return }
|
||||
val schemeAdmin = try { myChanel.getBoolean("scheme_admin") } catch (e: JSONException) { false }
|
||||
val id = "$channelId-$userId"
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ChannelMembership
|
||||
(id, channel_id, user_id, scheme_admin, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""",
|
||||
arrayOf(id, channelId, userId, schemeAdmin)
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMyChannel(db: WMDatabase, myChanel: JSONObject) {
|
||||
try {
|
||||
val id = try { myChanel.getString("id") } catch (e: JSONException) { return }
|
||||
val msgCount = try { myChanel.getInt("message_count") } catch (e: JSONException) { 0 }
|
||||
val mentionsCount = try { myChanel.getInt("mentions_count") } catch (e: JSONException) { 0 }
|
||||
val isUnread = try { myChanel.getBoolean("is_unread") } catch (e: JSONException) { false }
|
||||
val lastPostAt = try { myChanel.getDouble("last_post_at") } catch (e: JSONException) { 0 }
|
||||
val lastViewedAt = try { myChanel.getDouble("last_viewed_at") } catch (e: JSONException) { 0 }
|
||||
val lastFetchedAt = try { myChanel.getDouble("last_fetched_at") } catch (e: JSONException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE MyChannel SET message_count=?, mentions_count=?, is_unread=?,
|
||||
last_post_at=?, last_viewed_at=?, last_fetched_at=?, _status = 'updated'
|
||||
WHERE id=?
|
||||
""",
|
||||
arrayOf(
|
||||
msgCount, mentionsCount, isUnread,
|
||||
lastPostAt, lastViewedAt, lastFetchedAt, id
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
import org.json.JSONArray
|
||||
|
||||
internal fun insertCustomEmojis(db: WMDatabase, customEmojis: JSONArray) {
|
||||
for (i in 0 until customEmojis.length()) {
|
||||
try {
|
||||
val emoji = customEmojis.getJSONObject(i)
|
||||
if (find(db, "CustomEmoji", emoji.getString("id")) == null) {
|
||||
db.execute(
|
||||
"INSERT INTO CustomEmoji (id, name, _changed, _status) VALUES (?, ?, '', 'created')",
|
||||
arrayOf(
|
||||
emoji.getString("id"),
|
||||
emoji.getString("name"),
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
|
||||
internal fun insertFiles(db: WMDatabase, files: JSONArray) {
|
||||
try {
|
||||
for (i in 0 until files.length()) {
|
||||
val file = files.getJSONObject(i)
|
||||
val id = file.getString("id")
|
||||
val extension = file.getString("extension")
|
||||
val miniPreview = try { file.getString("mini_preview") } catch (e: JSONException) { "" }
|
||||
val height = try { file.getInt("height") } catch (e: JSONException) { 0 }
|
||||
val mime = file.getString("mime_type")
|
||||
val name = file.getString("name")
|
||||
val postId = file.getString("post_id")
|
||||
val size = try { file.getDouble("size") } catch (e: JSONException) { 0 }
|
||||
val width = try { file.getInt("width") } catch (e: JSONException) { 0 }
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO File
|
||||
(id, extension, height, image_thumbnail, local_path, mime_type, name, post_id, size, width, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, extension, height, miniPreview,
|
||||
mime, name, postId, size, width
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.text.TextUtils
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.DatabaseHelper
|
||||
import com.mattermost.helpers.QueryArgs
|
||||
import com.mattermost.helpers.mapCursor
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
import java.util.Arrays
|
||||
|
||||
internal fun DatabaseHelper.saveToDatabase(db: WMDatabase, data: ReadableMap, teamId: String?, channelId: String?, receivingThreads: Boolean) {
|
||||
db.transaction {
|
||||
val posts = data.getMap("posts")
|
||||
data.getMap("team")?.let { insertTeam(db, it) }
|
||||
data.getMap("myTeam")?.let { insertMyTeam(db, it) }
|
||||
data.getMap("channel")?.let { handleChannel(db, it) }
|
||||
data.getMap("myChannel")?.let { handleMyChannel(db, it, posts, receivingThreads) }
|
||||
data.getMap("categories")?.let { insertCategoriesWithChannels(db, it) }
|
||||
data.getArray("categoryChannels")?.let { insertChannelToDefaultCategory(db, it) }
|
||||
if (channelId != null) {
|
||||
handlePosts(db, posts, channelId, receivingThreads)
|
||||
}
|
||||
data.getArray("threads")?.let {
|
||||
val threadsArray = ArrayList<ReadableMap>()
|
||||
for (i in 0 until it.size()) {
|
||||
threadsArray.add(it.getMap(i))
|
||||
}
|
||||
handleThreads(db, threadsArray, teamId)
|
||||
}
|
||||
data.getArray("users")?.let { handleUsers(db, it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun DatabaseHelper.getServerUrlForIdentifier(identifier: String): String? {
|
||||
try {
|
||||
val query = "SELECT url FROM Servers WHERE identifier=?"
|
||||
defaultDatabase!!.rawQuery(query, arrayOf(identifier)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getString(0)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): WMDatabase? {
|
||||
try {
|
||||
val query = "SELECT db_path FROM Servers WHERE url=?"
|
||||
defaultDatabase!!.rawQuery(query, arrayOf(serverUrl)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val databasePath = String.format("file://%s", cursor.getString(0))
|
||||
return WMDatabase.buildDatabase(databasePath, context!!, SQLiteDatabase.CREATE_IF_NECESSARY)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun DatabaseHelper.getDeviceToken(): String? {
|
||||
try {
|
||||
val query = "SELECT value FROM Global WHERE id=?"
|
||||
defaultDatabase!!.rawQuery(query, arrayOf("deviceToken")).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getString(0)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun find(db: WMDatabase, tableName: String, id: String?): ReadableMap? {
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT * FROM $tableName WHERE id == ? LIMIT 1",
|
||||
arrayOf(id)
|
||||
).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return null
|
||||
}
|
||||
val resultMap = Arguments.createMap()
|
||||
cursor.moveToFirst()
|
||||
resultMap.mapCursor(cursor)
|
||||
return resultMap
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun findByColumns(db: WMDatabase, tableName: String, columnNames: Array<String>, values: QueryArgs): ReadableMap? {
|
||||
try {
|
||||
val whereString = columnNames.joinToString(" AND ") { "$it = ?" }
|
||||
db.rawQuery(
|
||||
"SELECT * FROM $tableName WHERE $whereString LIMIT 1",
|
||||
values
|
||||
).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return null
|
||||
}
|
||||
val resultMap = Arguments.createMap()
|
||||
cursor.moveToFirst()
|
||||
resultMap.mapCursor(cursor)
|
||||
return resultMap
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun queryIds(db: WMDatabase, tableName: String, ids: Array<String>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(ids).map { "?" }.toArray())
|
||||
try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
db.rawQuery("SELECT DISTINCT id FROM $tableName WHERE id IN ($args)", ids as Array<Any?>).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex("id")
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun queryByColumn(db: WMDatabase, tableName: String, columnName: String, values: Array<Any?>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(values).map { "?" }.toArray())
|
||||
try {
|
||||
db.rawQuery("SELECT DISTINCT $columnName FROM $tableName WHERE $columnName IN ($args)", values).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex(columnName)
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun countByColumn(db: WMDatabase, tableName: String, columnName: String, value: Any?): Int {
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT COUNT(*) FROM $tableName WHERE $columnName == ? LIMIT 1",
|
||||
arrayOf(value)
|
||||
).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return 0
|
||||
}
|
||||
cursor.moveToFirst()
|
||||
return cursor.getInt(0)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.DatabaseHelper
|
||||
import com.mattermost.helpers.ReadableMapUtils
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import kotlin.Exception
|
||||
|
||||
internal fun queryLastPostCreateAt(db: WMDatabase?, channelId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT earliest, latest FROM PostsInChannel WHERE channel_id=? ORDER BY latest DESC LIMIT 1"
|
||||
db.rawQuery(postsInChannelQuery, arrayOf(channelId)).use { cursor1 ->
|
||||
if (cursor1.count == 1) {
|
||||
cursor1.moveToFirst()
|
||||
val earliest = cursor1.getDouble(0)
|
||||
val latest = cursor1.getDouble(1)
|
||||
val postQuery = "SELECT create_at FROM POST WHERE channel_id= ? AND delete_at=0 AND create_at BETWEEN ? AND ? ORDER BY create_at DESC"
|
||||
|
||||
db.rawQuery(postQuery, arrayOf(channelId, earliest, latest)).use { cursor2 ->
|
||||
if (cursor2.count >= 60) {
|
||||
cursor2.moveToFirst()
|
||||
return cursor2.getDouble(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun queryPostSinceForChannel(db: WMDatabase?, channelId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
|
||||
db.rawQuery(postsInChannelQuery, arrayOf(channelId)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val lastFetchedAt = cursor.getDouble(0)
|
||||
if (lastFetchedAt == 0.0) {
|
||||
return queryLastPostCreateAt(db, channelId)
|
||||
}
|
||||
return lastFetchedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun queryLastPostInThread(db: WMDatabase?, rootId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
val query = "SELECT create_at FROM Post WHERE root_id=? AND delete_at=0 ORDER BY create_at DESC LIMIT 1"
|
||||
db.rawQuery(query, arrayOf(rootId)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getDouble(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun insertPost(db: WMDatabase, post: JSONObject) {
|
||||
try {
|
||||
val id = try { post.getString("id") } catch (e: JSONException) { return }
|
||||
val channelId = try { post.getString("channel_id") } catch (e: JSONException) { return }
|
||||
val userId = try { post.getString("user_id") } catch (e: JSONException) { return }
|
||||
val createAt = try { post.getDouble("create_at") } catch (e: JSONException) { return }
|
||||
val deleteAt = try { post.getDouble("delete_at") } catch (e: JSONException) { 0 }
|
||||
val updateAt = try { post.getDouble("update_at") } catch (e: JSONException) { 0 }
|
||||
val editAt = try { post.getDouble("edit_at") } catch (e: JSONException) { 0 }
|
||||
val isPinned = try { post.getBoolean("is_pinned") } catch (e: JSONException) { false }
|
||||
val message = try { post.getString("message") } catch (e: JSONException) { "" }
|
||||
val messageSource = try { post.getString("message_source") } catch (e: JSONException) { "" }
|
||||
val metadata = try { post.getJSONObject("metadata") } catch (e: JSONException) { JSONObject() }
|
||||
val originalId = try { post.getString("original_id") } catch (e: JSONException) { "" }
|
||||
val pendingId = try { post.getString("pending_post_id") } catch (e: JSONException) { "" }
|
||||
val prevId = try { post.getString("prev_post_id") } catch (e: JSONException) { "" }
|
||||
val rootId = try { post.getString("root_id") } catch (e: JSONException) { "" }
|
||||
val type = try { post.getString("type") } catch (e: JSONException) { "" }
|
||||
val props = try { post.getJSONObject("props").toString() } catch (e: JSONException) { "" }
|
||||
val reactions = metadata.remove("reactions") as JSONArray?
|
||||
val customEmojis = metadata.remove("emojis") as JSONArray?
|
||||
val files = metadata.remove("files") as JSONArray?
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Post
|
||||
(id, channel_id, create_at, delete_at, update_at, edit_at, is_pinned, message, message_source, metadata, original_id, pending_post_id,
|
||||
previous_post_id, root_id, type, user_id, props, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, channelId, createAt, deleteAt, updateAt, editAt,
|
||||
isPinned, message, messageSource, metadata.toString(),
|
||||
originalId, pendingId, prevId, rootId,
|
||||
type, userId, props
|
||||
)
|
||||
)
|
||||
|
||||
if (reactions != null && reactions.length() > 0) {
|
||||
insertReactions(db, reactions)
|
||||
}
|
||||
|
||||
if (customEmojis != null && customEmojis.length() > 0) {
|
||||
insertCustomEmojis(db, customEmojis)
|
||||
}
|
||||
|
||||
if (files != null && files.length() > 0) {
|
||||
insertFiles(db, files)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun updatePost(db: WMDatabase, post: JSONObject) {
|
||||
try {
|
||||
val id = try { post.getString("id") } catch (e: JSONException) { return }
|
||||
val channelId = try { post.getString("channel_id") } catch (e: JSONException) { return }
|
||||
val userId = try { post.getString("user_id") } catch (e: JSONException) { return }
|
||||
val createAt = try { post.getDouble("create_at") } catch (e: JSONException) { return }
|
||||
val deleteAt = try { post.getDouble("delete_at") } catch (e: JSONException) { 0 }
|
||||
val updateAt = try { post.getDouble("update_at") } catch (e: JSONException) { 0 }
|
||||
val editAt = try { post.getDouble("edit_at") } catch (e: JSONException) { 0 }
|
||||
val isPinned = try { post.getBoolean("is_pinned") } catch (e: JSONException) { false }
|
||||
val message = try { post.getString("message") } catch (e: JSONException) { "" }
|
||||
val messageSource = try { post.getString("message_source") } catch (e: JSONException) { "" }
|
||||
val metadata = try { post.getJSONObject("metadata") } catch (e: JSONException) { JSONObject() }
|
||||
val originalId = try { post.getString("original_id") } catch (e: JSONException) { "" }
|
||||
val pendingId = try { post.getString("pending_post_id") } catch (e: JSONException) { "" }
|
||||
val prevId = try { post.getString("prev_post_id") } catch (e: JSONException) { "" }
|
||||
val rootId = try { post.getString("root_id") } catch (e: JSONException) { "" }
|
||||
val type = try { post.getString("type") } catch (e: JSONException) { "" }
|
||||
val props = try { post.getJSONObject("props").toString() } catch (e: JSONException) { "" }
|
||||
val reactions = metadata.remove("reactions") as JSONArray?
|
||||
val customEmojis = metadata.remove("emojis") as JSONArray?
|
||||
|
||||
metadata.remove("files")
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE Post SET channel_id = ?, create_at = ?, delete_at = ?, update_at =?, edit_at =?,
|
||||
is_pinned = ?, message = ?, message_source = ?, metadata = ?, original_id = ?, pending_post_id = ?, previous_post_id = ?,
|
||||
root_id = ?, type = ?, user_id = ?, props = ?, _status = 'updated'
|
||||
WHERE id = ?
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
channelId, createAt, deleteAt, updateAt, editAt,
|
||||
isPinned, message, messageSource, metadata.toString(),
|
||||
originalId, pendingId, prevId, rootId,
|
||||
type, userId, props,
|
||||
id,
|
||||
)
|
||||
)
|
||||
|
||||
if (reactions != null && reactions.length() > 0) {
|
||||
db.execute("DELETE FROM Reaction WHERE post_id = ?", arrayOf(post.getString("id")))
|
||||
insertReactions(db, reactions)
|
||||
}
|
||||
|
||||
if (customEmojis != null && customEmojis.length() > 0) {
|
||||
insertCustomEmojis(db, customEmojis)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun DatabaseHelper.handlePosts(db: WMDatabase, postsData: ReadableMap?, channelId: String, receivingThreads: Boolean) {
|
||||
// Posts, PostInChannel, PostInThread, Reactions, Files, CustomEmojis, Users
|
||||
try {
|
||||
if (postsData != null) {
|
||||
val ordered = postsData.getArray("order")?.toArrayList()
|
||||
val posts = ReadableMapUtils.toJSONObject(postsData.getMap("posts")).toMap()
|
||||
val previousPostId = postsData.getString("prev_post_id")
|
||||
val postsInThread = hashMapOf<String, List<JSONObject>>()
|
||||
val postList = posts.toList()
|
||||
var earliest = 0.0
|
||||
var latest = 0.0
|
||||
|
||||
if (ordered != null && posts.isNotEmpty()) {
|
||||
val firstId = ordered.first()
|
||||
val lastId = ordered.last()
|
||||
var prevPostId = ""
|
||||
|
||||
val sortedPosts = postList.sortedBy { (_, value) ->
|
||||
((value as Map<*, *>)["create_at"] as Double)
|
||||
}
|
||||
|
||||
sortedPosts.forEachIndexed { index, it ->
|
||||
val key = it.first
|
||||
if (it.second != null) {
|
||||
@Suppress("UNCHECKED_CAST", "UNCHECKED_CAST")
|
||||
val post: MutableMap<String, Any?> = it.second as MutableMap<String, Any?>
|
||||
|
||||
if (index == 0) {
|
||||
post.putIfAbsent("prev_post_id", previousPostId)
|
||||
} else if (prevPostId.isNotEmpty()) {
|
||||
post.putIfAbsent("prev_post_id", prevPostId)
|
||||
}
|
||||
|
||||
if (lastId == key) {
|
||||
earliest = post["create_at"] as Double
|
||||
}
|
||||
if (firstId == key) {
|
||||
latest = post["create_at"] as Double
|
||||
}
|
||||
|
||||
val jsonPost = JSONObject(post)
|
||||
val postId = post["id"] as? String ?: ""
|
||||
val rootId = post["root_id"] as? String ?: ""
|
||||
val postInThread = rootId.ifEmpty { postId }
|
||||
var thread = postsInThread[postInThread]?.toMutableList()
|
||||
if (thread == null) {
|
||||
thread = mutableListOf()
|
||||
}
|
||||
|
||||
thread.add(jsonPost)
|
||||
postsInThread[postInThread] = thread.toList()
|
||||
|
||||
if (find(db, "Post", key) == null) {
|
||||
insertPost(db, jsonPost)
|
||||
} else {
|
||||
updatePost(db, jsonPost)
|
||||
}
|
||||
|
||||
if (ordered.contains(key)) {
|
||||
prevPostId = key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!receivingThreads) {
|
||||
handlePostsInChannel(db, channelId, earliest, latest)
|
||||
}
|
||||
handlePostsInThread(db, postsInThread)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.RandomId
|
||||
import com.mattermost.helpers.mapCursor
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
|
||||
internal fun findPostInChannel(chunks: ReadableArray, earliest: Double, latest: Double): ReadableMap? {
|
||||
for (i in 0 until chunks.size()) {
|
||||
val chunk = chunks.getMap(i)
|
||||
if (earliest >= chunk.getDouble("earliest") || latest <= chunk.getDouble("latest")) {
|
||||
return chunk
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun insertPostInChannel(db: WMDatabase, channelId: String, earliest: Double, latest: Double): ReadableMap? {
|
||||
return try {
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO PostsInChannel
|
||||
(id, channel_id, earliest, latest, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, channelId, earliest, latest))
|
||||
|
||||
val map = Arguments.createMap()
|
||||
map.putString("id", id)
|
||||
map.putString("channel_id", channelId)
|
||||
map.putDouble("earliest", earliest)
|
||||
map.putDouble("latest", latest)
|
||||
map
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun mergePostsInChannel(db: WMDatabase, existingChunks: ReadableArray, newChunk: ReadableMap) {
|
||||
for (i in 0 until existingChunks.size()) {
|
||||
try {
|
||||
val chunk = existingChunks.getMap(i)
|
||||
if (newChunk.getDouble("earliest") <= chunk.getDouble("earliest") &&
|
||||
newChunk.getDouble("latest") >= chunk.getDouble("latest")) {
|
||||
db.execute("DELETE FROM PostsInChannel WHERE id = ?", arrayOf(chunk.getString("id")))
|
||||
break
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun handlePostsInChannel(db: WMDatabase, channelId: String, earliest: Double, latest: Double) {
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT id, channel_id, earliest, latest FROM PostsInChannel WHERE channel_id = ?",
|
||||
arrayOf(channelId)
|
||||
).use { cursor ->
|
||||
if (cursor.count == 0) {
|
||||
// create new post in channel
|
||||
insertPostInChannel(db, channelId, earliest, latest)
|
||||
return
|
||||
}
|
||||
|
||||
val resultArray = Arguments.createArray()
|
||||
while (cursor.moveToNext()) {
|
||||
val cursorMap = Arguments.createMap()
|
||||
cursorMap.mapCursor(cursor)
|
||||
resultArray.pushMap(cursorMap)
|
||||
}
|
||||
|
||||
val chunk = findPostInChannel(resultArray, earliest, latest)
|
||||
if (chunk != null) {
|
||||
db.execute(
|
||||
"UPDATE PostsInChannel SET earliest = ?, latest = ?, _status = 'updated' WHERE id = ?",
|
||||
arrayOf(
|
||||
minOf(earliest, chunk.getDouble("earliest")),
|
||||
maxOf(latest, chunk.getDouble("latest")),
|
||||
chunk.getString("id")
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val newChunk = insertPostInChannel(db, channelId, earliest, latest)
|
||||
newChunk?.let { mergePostsInChannel(db, resultArray, it) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.mattermost.helpers.mapCursor
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
|
||||
fun getTeammateDisplayNameSetting(db: WMDatabase): String {
|
||||
val configSetting = queryConfigDisplayNameSetting(db)
|
||||
if (configSetting != null) {
|
||||
return configSetting
|
||||
}
|
||||
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT value FROM Preference where category = ? AND name = ? limit 1",
|
||||
arrayOf("display_settings", "name_format")
|
||||
).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return "username"
|
||||
}
|
||||
val resultMap = Arguments.createMap()
|
||||
cursor.moveToFirst()
|
||||
resultMap.mapCursor(cursor)
|
||||
return resultMap?.getString("value") ?: "username"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return "username"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.mattermost.helpers.RandomId
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
import org.json.JSONArray
|
||||
|
||||
internal fun insertReactions(db: WMDatabase, reactions: JSONArray) {
|
||||
for (i in 0 until reactions.length()) {
|
||||
try {
|
||||
val reaction = reactions.getJSONObject(i)
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Reaction
|
||||
(id, create_at, emoji_name, post_id, user_id, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id,
|
||||
reaction.getDouble("create_at"), reaction.getString("emoji_name"),
|
||||
reaction.getString("post_id"), reaction.getString("user_id")
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
import org.json.JSONObject
|
||||
|
||||
fun queryCurrentUserId(db: WMDatabase): String? {
|
||||
val result = find(db, "System", "currentUserId")
|
||||
return result?.getString("value")?.removeSurrounding("\"")
|
||||
}
|
||||
|
||||
fun queryCurrentTeamId(db: WMDatabase): String? {
|
||||
val result = find(db, "System", "currentTeamId")
|
||||
return result?.getString("value")?.removeSurrounding("\"")
|
||||
}
|
||||
|
||||
fun queryConfigDisplayNameSetting(db: WMDatabase): String? {
|
||||
val license = find(db, "System", "license")
|
||||
val lockDisplayName = find(db, "Config", "LockTeammateNameDisplay")
|
||||
val displayName = find(db, "Config", "TeammateNameDisplay")
|
||||
|
||||
val licenseValue = license?.getString("value") ?: ""
|
||||
val lockDisplayNameValue = lockDisplayName?.getString("value") ?: "false"
|
||||
val displayNameValue = displayName?.getString("value") ?: "full_name"
|
||||
val licenseJson = JSONObject(licenseValue)
|
||||
val licenseLock = try { licenseJson.getString("LockTeammateNameDisplay") } catch (e: Exception) { "false"}
|
||||
|
||||
if (licenseLock == "true" && lockDisplayNameValue == "true") {
|
||||
return displayNameValue
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun queryConfigSigningKey(db: WMDatabase): String? {
|
||||
return find(db, "Config", "AsymmetricSigningPublicKey")?.getString("value")
|
||||
}
|
||||
|
||||
fun queryConfigServerVersion(db: WMDatabase): String? {
|
||||
return find(db, "Config", "Version")?.getString("value")
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.mapCursor
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
|
||||
fun findTeam(db: WMDatabase?, teamId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "Team", teamId)
|
||||
return team != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun findMyTeam(db: WMDatabase?, teamId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "MyTeam", teamId)
|
||||
return team != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun queryMyTeams(db: WMDatabase?): ArrayList<ReadableMap>? {
|
||||
db?.rawQuery("SELECT * FROM MyTeam")?.use { cursor ->
|
||||
val results = ArrayList<ReadableMap>()
|
||||
if (cursor.count > 0) {
|
||||
while(cursor.moveToNext()) {
|
||||
val map = Arguments.createMap()
|
||||
map.mapCursor(cursor)
|
||||
results.add(map)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun insertTeam(db: WMDatabase, team: ReadableMap): Boolean {
|
||||
val id = try { team.getString("id") } catch (e: Exception) { return false }
|
||||
val deleteAt = try {team.getDouble("delete_at") } catch (e: Exception) { 0 }
|
||||
if (deleteAt.toInt() > 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
val isAllowOpenInvite = try { team.getBoolean("allow_open_invite") } catch (e: NoSuchKeyException) { false }
|
||||
val description = try { team.getString("description") } catch (e: NoSuchKeyException) { "" }
|
||||
val displayName = try { team.getString("display_name") } catch (e: NoSuchKeyException) { "" }
|
||||
val name = try { team.getString("name") } catch (e: NoSuchKeyException) { "" }
|
||||
val updateAt = try { team.getDouble("update_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val type = try { team.getString("type") } catch (e: NoSuchKeyException) { "O" }
|
||||
val allowedDomains = try { team.getString("allowed_domains") } catch (e: NoSuchKeyException) { "" }
|
||||
val isGroupConstrained = try { team.getBoolean("group_constrained") } catch (e: NoSuchKeyException) { false }
|
||||
val lastTeamIconUpdatedAt = try { team.getDouble("last_team_icon_update") } catch (e: NoSuchKeyException) { 0 }
|
||||
val inviteId = try { team.getString("invite_id") } catch (e: NoSuchKeyException) { "" }
|
||||
val status = "created"
|
||||
|
||||
return try {
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Team (
|
||||
id, allow_open_invite, description, display_name, name, update_at, type, allowed_domains,
|
||||
group_constrained, last_team_icon_update, invite_id, _changed, _status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?)
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, isAllowOpenInvite, description, displayName, name, updateAt,
|
||||
type, allowedDomains, isGroupConstrained, lastTeamIconUpdatedAt, inviteId, status
|
||||
)
|
||||
)
|
||||
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun insertMyTeam(db: WMDatabase, myTeam: ReadableMap): Boolean {
|
||||
val currentUserId = queryCurrentUserId(db) ?: return false
|
||||
val id = try { myTeam.getString("id") } catch (e: NoSuchKeyException) { return false }
|
||||
val roles = try { myTeam.getString("roles") } catch (e: NoSuchKeyException) { "" }
|
||||
val schemeAdmin = try { myTeam.getBoolean("scheme_admin") } catch (e: NoSuchKeyException) { false }
|
||||
val status = "created"
|
||||
val membershipId = "$id-$currentUserId"
|
||||
|
||||
return try {
|
||||
db.execute(
|
||||
"INSERT INTO MyTeam (id, roles, _changed, _status) VALUES (?, ?, '', ?)",
|
||||
arrayOf(id, roles, status)
|
||||
)
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO TeamMembership (id, team_id, user_id, scheme_admin, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', ?)
|
||||
""".trimIndent(),
|
||||
arrayOf(membershipId, id, currentUserId, schemeAdmin, status)
|
||||
)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import android.util.Log
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.RandomId
|
||||
import com.mattermost.helpers.mapCursor
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
import org.json.JSONObject
|
||||
|
||||
private fun getLastReplyAt(thread: ReadableMap): Double {
|
||||
try {
|
||||
var v = thread.getDouble("last_reply_at")
|
||||
if (v == 0.0) {
|
||||
val post = thread.getMap("post")
|
||||
if (post != null) {
|
||||
v = post.getDouble("create_at")
|
||||
}
|
||||
}
|
||||
return v
|
||||
} catch (e: NoSuchKeyException) {
|
||||
return 0.0
|
||||
}
|
||||
}
|
||||
|
||||
internal fun insertThread(db: WMDatabase, thread: ReadableMap) {
|
||||
// These fields are not present when we extract threads from posts
|
||||
try {
|
||||
val id = try { thread.getString("id") } catch (e: NoSuchKeyException) { return }
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { false }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { 0 }
|
||||
val lastReplyAt = getLastReplyAt(thread)
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Thread
|
||||
(id, last_reply_at, last_fetched_at, last_viewed_at, reply_count, is_following, unread_replies, unread_mentions, viewed_at, _changed, _status)
|
||||
VALUES (?, ?, 0, ?, ?, ?, ?, ?, 0, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, lastReplyAt, lastViewedAt,
|
||||
replyCount, isFollowing, unreadReplies, unreadMentions
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun updateThread(db: WMDatabase, thread: ReadableMap, existingRecord: ReadableMap) {
|
||||
try {
|
||||
// These fields are not present when we extract threads from posts
|
||||
val id = try { thread.getString("id") } catch (e: NoSuchKeyException) { return }
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { existingRecord.getInt("is_following") == 1 }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { existingRecord.getDouble("last_viewed_at") }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_replies") }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_mentions") }
|
||||
val lastReplyAt = getLastReplyAt(thread)
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE Thread SET
|
||||
last_reply_at = ?, last_viewed_at = ?, reply_count = ?, is_following = ?, unread_replies = ?,
|
||||
unread_mentions = ?, _status = 'updated' where id = ?
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
lastReplyAt, lastViewedAt, replyCount,
|
||||
isFollowing, unreadReplies, unreadMentions, id
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun insertThreadParticipants(db: WMDatabase, threadId: String, participants: ReadableArray) {
|
||||
for (i in 0 until participants.size()) {
|
||||
try {
|
||||
val participant = participants.getMap(i)
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ThreadParticipant
|
||||
(id, thread_id, user_id, _changed, _status)
|
||||
VALUES (?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, threadId, participant.getString("id"))
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun insertTeamThreadsSync(db: WMDatabase, teamId: String, earliest: Double, latest: Double) {
|
||||
try {
|
||||
val query = """
|
||||
INSERT INTO TeamThreadsSync (id, _changed, _status, earliest, latest)
|
||||
VALUES (?, '', 'created', ?, ?)
|
||||
"""
|
||||
db.execute(query, arrayOf(teamId, earliest, latest))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTeamThreadsSync(db: WMDatabase, teamId: String, earliest: Double, latest: Double, existingRecord: ReadableMap) {
|
||||
try {
|
||||
val storeEarliest = minOf(earliest, existingRecord.getDouble("earliest"))
|
||||
val storeLatest = maxOf(latest, existingRecord.getDouble("latest"))
|
||||
val query = "UPDATE TeamThreadsSync SET earliest=?, latest=? WHERE id=?"
|
||||
db.execute(query, arrayOf(storeEarliest, storeLatest, teamId))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun syncParticipants(db: WMDatabase, thread: ReadableMap) {
|
||||
try {
|
||||
val threadId = thread.getString("id")
|
||||
val participants = thread.getArray("participants")
|
||||
if (participants != null) {
|
||||
db.execute("DELETE FROM ThreadParticipant WHERE thread_id = ?", arrayOf(threadId))
|
||||
|
||||
if (participants.size() > 0) {
|
||||
insertThreadParticipants(db, threadId!!, participants)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun handlePostsInThread(db: WMDatabase, postsInThread: Map<String, List<JSONObject>>) {
|
||||
postsInThread.forEach { (key, list) ->
|
||||
try {
|
||||
val sorted = list.sortedBy { it.getDouble("create_at") }
|
||||
val earliest = sorted.first().getDouble("create_at")
|
||||
val latest = sorted.last().getDouble("create_at")
|
||||
db.rawQuery("SELECT * FROM PostsInThread WHERE root_id = ? ORDER BY latest DESC", arrayOf(key)).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
cursor.moveToFirst()
|
||||
val cursorMap = Arguments.createMap()
|
||||
cursorMap.mapCursor(cursor)
|
||||
val storeEarliest = minOf(earliest, cursorMap.getDouble("earliest"))
|
||||
val storeLatest = maxOf(latest, cursorMap.getDouble("latest"))
|
||||
db.execute(
|
||||
"UPDATE PostsInThread SET earliest = ?, latest = ?, _status = 'updated' WHERE root_id = ?",
|
||||
arrayOf(
|
||||
storeEarliest,
|
||||
storeLatest,
|
||||
key
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO PostsInThread
|
||||
(id, root_id, earliest, latest, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, key, earliest, latest)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleThreads(db: WMDatabase, threads: ArrayList<ReadableMap>, teamId: String?) {
|
||||
val teamIds = ArrayList<String>()
|
||||
if (teamId.isNullOrEmpty()) {
|
||||
val myTeams = queryMyTeams(db)
|
||||
if (myTeams != null) {
|
||||
for (myTeam in myTeams) {
|
||||
myTeam.getString("id")?.let { teamIds.add(it) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
teamIds.add(teamId)
|
||||
}
|
||||
|
||||
for (i in 0 until threads.size) {
|
||||
try {
|
||||
val thread = threads[i]
|
||||
handleThread(db, thread, teamIds)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
handleTeamThreadsSync(db, threads, teamIds)
|
||||
}
|
||||
|
||||
fun handleThread(db: WMDatabase, thread: ReadableMap, teamIds: ArrayList<String>) {
|
||||
// Insert/Update the thread
|
||||
val threadId = thread.getString("id")
|
||||
val isFollowing = thread.getBoolean("is_following")
|
||||
val existingRecord = find(db, "Thread", threadId)
|
||||
if (existingRecord == null) {
|
||||
insertThread(db, thread)
|
||||
} else {
|
||||
updateThread(db, thread, existingRecord)
|
||||
}
|
||||
|
||||
syncParticipants(db, thread)
|
||||
|
||||
// this is per team
|
||||
if (isFollowing) {
|
||||
for (teamId in teamIds) {
|
||||
handleThreadInTeam(db, thread, teamId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleThreadInTeam(db: WMDatabase, thread: ReadableMap, teamId: String) {
|
||||
val threadId = thread.getString("id") ?: return
|
||||
val existingRecord = findByColumns(
|
||||
db,
|
||||
"ThreadsInTeam",
|
||||
arrayOf("thread_id", "team_id"),
|
||||
arrayOf(threadId, teamId)
|
||||
)
|
||||
if (existingRecord == null) {
|
||||
try {
|
||||
val id = RandomId.generate()
|
||||
val query = """
|
||||
INSERT INTO ThreadsInTeam (id, team_id, thread_id, _changed, _status)
|
||||
VALUES (?, ?, ?, '', 'created')
|
||||
"""
|
||||
db.execute(query, arrayOf(id, teamId, threadId))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleTeamThreadsSync(db: WMDatabase, threadList: ArrayList<ReadableMap>, teamIds: ArrayList<String>) {
|
||||
val sortedList = threadList.filter{ it.getBoolean("is_following") }
|
||||
.sortedBy {
|
||||
var v = getLastReplyAt(it)
|
||||
if (v == 0.0) {
|
||||
Log.d("Database", "Trying to add a thread with no replies and no post")
|
||||
}
|
||||
v
|
||||
}
|
||||
.map {
|
||||
getLastReplyAt(it)
|
||||
}
|
||||
if (sortedList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
val earliest = sortedList.first()
|
||||
val latest = sortedList.last()
|
||||
|
||||
for (teamId in teamIds) {
|
||||
val existingTeamThreadsSync = find(db, "TeamThreadsSync", teamId)
|
||||
if (existingTeamThreadsSync == null) {
|
||||
insertTeamThreadsSync(db, teamId, earliest, latest)
|
||||
} else {
|
||||
updateTeamThreadsSync(db, teamId, earliest, latest, existingTeamThreadsSync)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.mattermost.helpers.ReadableMapUtils
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
|
||||
fun getLastPictureUpdate(db: WMDatabase?, userId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
var id = userId
|
||||
if (userId == "me") {
|
||||
(queryCurrentUserId(db) ?: userId).also { id = it }
|
||||
}
|
||||
val userQuery = "SELECT last_picture_update FROM User WHERE id=?"
|
||||
db.rawQuery(userQuery, arrayOf(id)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getDouble(0)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getCurrentUserLocale(db: WMDatabase): String {
|
||||
try {
|
||||
val currentUserId = queryCurrentUserId(db) ?: return "en"
|
||||
val userQuery = "SELECT locale FROM User WHERE id=?"
|
||||
db.rawQuery(userQuery, arrayOf(currentUserId)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getString(0)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return "en"
|
||||
}
|
||||
|
||||
fun handleUsers(db: WMDatabase, users: ReadableArray) {
|
||||
for (i in 0 until users.size()) {
|
||||
val user = users.getMap(i)
|
||||
val roles = user.getString("roles") ?: ""
|
||||
val isBot = try {
|
||||
user.getBoolean("is_bot")
|
||||
} catch (e: NoSuchKeyException) {
|
||||
false
|
||||
}
|
||||
|
||||
val lastPictureUpdate = try { user.getDouble("last_picture_update") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
try {
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO User (id, auth_service, update_at, delete_at, email, first_name, is_bot, is_guest,
|
||||
last_name, last_picture_update, locale, nickname, position, roles, status, username, notify_props,
|
||||
props, timezone, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
user.getString("id"),
|
||||
user.getString("auth_service"), user.getDouble("update_at"), user.getDouble("delete_at"),
|
||||
user.getString("email"), user.getString("first_name"), isBot,
|
||||
roles.contains("system_guest"), user.getString("last_name"), lastPictureUpdate,
|
||||
user.getString("locale"), user.getString("nickname"), user.getString("position"),
|
||||
roles, "", user.getString("username"), "{}",
|
||||
ReadableMapUtils.toJSONObject(user.getMap("props")
|
||||
?: Arguments.createMap()).toString(),
|
||||
ReadableMapUtils.toJSONObject(user.getMap("timezone")
|
||||
?: Arguments.createMap()).toString(),
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.database_extension.findByColumns
|
||||
import com.mattermost.helpers.database_extension.queryCurrentUserId
|
||||
import com.mattermost.helpers.database_extension.queryMyTeams
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
|
||||
suspend fun PushNotificationDataRunnable.Companion.fetchMyTeamCategories(db: WMDatabase, serverUrl: String, teamId: String): ReadableMap? {
|
||||
return try {
|
||||
val userId = queryCurrentUserId(db)
|
||||
val categories = fetch(serverUrl, "/api/v4/users/$userId/teams/$teamId/channels/categories")
|
||||
categories?.getMap("data")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun PushNotificationDataRunnable.Companion.addToDefaultCategoryIfNeeded(db: WMDatabase, channel: ReadableMap): ReadableArray? {
|
||||
val channelId = channel.getString("id") ?: return null
|
||||
val channelType = channel.getString("type")
|
||||
val categoryChannels = Arguments.createArray()
|
||||
if (channelType == "D" || channelType == "G") {
|
||||
val myTeams = queryMyTeams(db)
|
||||
myTeams?.let {
|
||||
for (myTeam in it) {
|
||||
val map = categoryChannelForTeam(db, channelId, myTeam.getString("id"), "direct_messages")
|
||||
if (map != null) {
|
||||
categoryChannels.pushMap(map)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val map = categoryChannelForTeam(db, channelId, channel.getString("team_id"), "channels")
|
||||
if (map != null) {
|
||||
categoryChannels.pushMap(map)
|
||||
}
|
||||
}
|
||||
|
||||
return categoryChannels
|
||||
}
|
||||
|
||||
private fun categoryChannelForTeam(db: WMDatabase, channelId: String, teamId: String?, type: String): ReadableMap? {
|
||||
teamId?.let { id ->
|
||||
val category = findByColumns(db, "Category", arrayOf("type", "team_id"), arrayOf(type, id))
|
||||
val categoryId = category?.getString("id")
|
||||
categoryId?.let { cId ->
|
||||
val cc = findByColumns(
|
||||
db,
|
||||
"CategoryChannel",
|
||||
arrayOf("category_id", "channel_id"),
|
||||
arrayOf(cId, channelId)
|
||||
)
|
||||
if (cc == null) {
|
||||
val map = Arguments.createMap()
|
||||
map.putString("channel_id", channelId)
|
||||
map.putString("category_id", cId)
|
||||
map.putString("id", "${id}_$channelId")
|
||||
return map
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.database_extension.findChannel
|
||||
import com.mattermost.helpers.database_extension.getCurrentUserLocale
|
||||
import com.mattermost.helpers.database_extension.getTeammateDisplayNameSetting
|
||||
import com.mattermost.helpers.database_extension.queryCurrentUserId
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
import java.text.Collator
|
||||
import java.util.Locale
|
||||
|
||||
suspend fun PushNotificationDataRunnable.Companion.fetchMyChannel(db: WMDatabase, serverUrl: String, channelId: String, isCRTEnabled: Boolean): Triple<ReadableMap?, ReadableMap?, ReadableArray?> {
|
||||
val channel = fetch(serverUrl, "/api/v4/channels/$channelId")
|
||||
var channelData = channel?.getMap("data")
|
||||
val myChannelData = channelData?.let { fetchMyChannelData(serverUrl, channelId, isCRTEnabled, it) }
|
||||
val channelType = channelData?.getString("type")
|
||||
var profilesArray: ReadableArray? = null
|
||||
|
||||
if (channelData != null && channelType != null && !findChannel(db, channelId)) {
|
||||
val displayNameSetting = getTeammateDisplayNameSetting(db)
|
||||
|
||||
when (channelType) {
|
||||
"D" -> {
|
||||
profilesArray = fetchProfileInChannel(db, serverUrl, channelId)
|
||||
if ((profilesArray?.size() ?: 0) > 0) {
|
||||
val displayName = displayUsername(profilesArray!!.getMap(0), displayNameSetting)
|
||||
val data = Arguments.createMap()
|
||||
data.merge(channelData)
|
||||
data.putString("display_name", displayName)
|
||||
channelData = data
|
||||
}
|
||||
}
|
||||
"G" -> {
|
||||
profilesArray = fetchProfileInChannel(db, serverUrl, channelId)
|
||||
if ((profilesArray?.size() ?: 0) > 0) {
|
||||
val localeString = getCurrentUserLocale(db)
|
||||
val localeArray = localeString.split("-")
|
||||
val locale = if (localeArray.size == 1) {
|
||||
Locale(localeString)
|
||||
} else {
|
||||
Locale(localeArray[0], localeArray[1])
|
||||
}
|
||||
val displayName = displayGroupMessageName(profilesArray!!, locale, displayNameSetting)
|
||||
val data = Arguments.createMap()
|
||||
data.merge(channelData)
|
||||
data.putString("display_name", displayName)
|
||||
channelData = data
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
return Triple(channelData, myChannelData, profilesArray)
|
||||
}
|
||||
|
||||
private suspend fun PushNotificationDataRunnable.Companion.fetchMyChannelData(serverUrl: String, channelId: String, isCRTEnabled: Boolean, channelData: ReadableMap): ReadableMap? {
|
||||
try {
|
||||
val myChannel = fetch(serverUrl, "/api/v4/channels/$channelId/members/me")
|
||||
val myChannelData = myChannel?.getMap("data")
|
||||
if (myChannelData != null) {
|
||||
val data = Arguments.createMap()
|
||||
data.merge(myChannelData)
|
||||
data.putString("id", channelId)
|
||||
|
||||
val totalMsg = if (isCRTEnabled) {
|
||||
channelData.getInt("total_msg_count_root")
|
||||
} else {
|
||||
channelData.getInt("total_msg_count")
|
||||
}
|
||||
|
||||
val myMsgCount = if (isCRTEnabled) {
|
||||
myChannelData.getInt("msg_count_root")
|
||||
} else {
|
||||
myChannelData.getInt("msg_count")
|
||||
}
|
||||
|
||||
val mentionCount = if (isCRTEnabled) {
|
||||
myChannelData.getInt("mention_count_root")
|
||||
} else {
|
||||
myChannelData.getInt("mention_count")
|
||||
}
|
||||
|
||||
val lastPostAt = if (isCRTEnabled) {
|
||||
try {
|
||||
channelData.getDouble("last_root_post_at")
|
||||
} catch (e: Exception) {
|
||||
channelData.getDouble("last_post_at")
|
||||
}
|
||||
} else {
|
||||
channelData.getDouble("last_post_at")
|
||||
}
|
||||
|
||||
val messageCount = 0.coerceAtLeast(totalMsg - myMsgCount)
|
||||
data.putInt("message_count", messageCount)
|
||||
data.putInt("mentions_count", mentionCount)
|
||||
data.putBoolean("is_unread", messageCount > 0)
|
||||
data.putDouble("last_post_at", lastPostAt)
|
||||
return data
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun PushNotificationDataRunnable.Companion.fetchProfileInChannel(db: WMDatabase, serverUrl: String, channelId: String): ReadableArray? {
|
||||
return try {
|
||||
val currentUserId = queryCurrentUserId(db)
|
||||
val profilesInChannel = fetch(serverUrl, "/api/v4/users?in_channel=${channelId}&page=0&per_page=8&sort=")
|
||||
val profilesArray = profilesInChannel?.getArray("data")
|
||||
val result = Arguments.createArray()
|
||||
if (profilesArray != null) {
|
||||
for (i in 0 until profilesArray.size()) {
|
||||
val profile = profilesArray.getMap(i)
|
||||
if (profile.getString("id") != currentUserId) {
|
||||
result.pushMap(profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun PushNotificationDataRunnable.Companion.displayUsername(user: ReadableMap, displayNameSetting: String): String {
|
||||
val name = user.getString("username") ?: ""
|
||||
val nickname = user.getString("nickname")
|
||||
val firstName = user.getString("first_name") ?: ""
|
||||
val lastName = user.getString("last_name") ?: ""
|
||||
return when (displayNameSetting) {
|
||||
"nickname_full_name" -> {
|
||||
(nickname ?: "$firstName $lastName").trim()
|
||||
}
|
||||
"full_name" -> {
|
||||
"$firstName $lastName".trim()
|
||||
}
|
||||
else -> {
|
||||
name.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PushNotificationDataRunnable.Companion.displayGroupMessageName(profilesArray: ReadableArray, locale: Locale, displayNameSetting: String): String {
|
||||
val names = ArrayList<String>()
|
||||
for (i in 0 until profilesArray.size()) {
|
||||
val profile = profilesArray.getMap(i)
|
||||
names.add(displayUsername(profile, displayNameSetting))
|
||||
}
|
||||
|
||||
return names.sortedWith { s1, s2 ->
|
||||
Collator.getInstance(locale).compare(s1, s2)
|
||||
}.joinToString(", ").trim()
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.Network
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.ResolvePromise
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetch(serverUrl: String, endpoint: String): ReadableMap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Network.get(serverUrl, endpoint, null, object : ResolvePromise() {
|
||||
override fun resolve(value: Any?) {
|
||||
val response = value as ReadableMap?
|
||||
if (response != null && !response.getBoolean("ok")) {
|
||||
val error = response.getMap("data")
|
||||
cont.resumeWith(Result.failure((IOException("Unexpected code ${error?.getInt("status_code")} ${error?.getString("message")}"))))
|
||||
} else {
|
||||
cont.resumeWith(Result.success(response))
|
||||
}
|
||||
}
|
||||
|
||||
override fun reject(code: String, message: String?) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
|
||||
}
|
||||
|
||||
override fun reject(throwable: Throwable) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $throwable")))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchWithPost(serverUrl: String, endpoint: String, options: ReadableMap?) : ReadableMap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Network.post(serverUrl, endpoint, options, object : ResolvePromise() {
|
||||
override fun resolve(value: Any?) {
|
||||
val response = value as ReadableMap?
|
||||
cont.resumeWith(Result.success(response))
|
||||
}
|
||||
|
||||
override fun reject(code: String, message: String?) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
|
||||
}
|
||||
|
||||
override fun reject(throwable: Throwable) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $throwable")))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.WritableNativeArray
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.ReadableArrayUtils
|
||||
import com.mattermost.helpers.ReadableMapUtils
|
||||
import com.mattermost.helpers.database_extension.*
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchPosts(
|
||||
db: WMDatabase, serverUrl: String, channelId: String, isCRTEnabled: Boolean,
|
||||
rootId: String?, loadedProfiles: ReadableArray?
|
||||
): ReadableMap? {
|
||||
return try {
|
||||
val regex = Regex("""\B@(([a-z\d-._]*[a-z\d_])[.-]*)""", setOf(RegexOption.IGNORE_CASE))
|
||||
val currentUserId = queryCurrentUserId(db)
|
||||
val currentUser = find(db, "User", currentUserId)
|
||||
val currentUsername = currentUser?.getString("username")
|
||||
|
||||
var additionalParams = ""
|
||||
if (isCRTEnabled) {
|
||||
additionalParams = "&collapsedThreads=true&collapsedThreadsExtended=true"
|
||||
}
|
||||
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
val endpoint = if (receivingThreads) {
|
||||
val since = rootId?.let { queryLastPostInThread(db, it) }
|
||||
val queryParams = if (since == null) "?perPage=60&fromCreatedAt=0&direction=up" else
|
||||
"?fromCreateAt=${since.toLong()}&direction=down"
|
||||
|
||||
"/api/v4/posts/$rootId/thread$queryParams$additionalParams"
|
||||
} else {
|
||||
val since = queryPostSinceForChannel(db, channelId)
|
||||
val queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}"
|
||||
"/api/v4/channels/$channelId/posts$queryParams$additionalParams"
|
||||
}
|
||||
|
||||
val postsResponse = fetch(serverUrl, endpoint)
|
||||
val postData = postsResponse?.getMap("data")
|
||||
val results = Arguments.createMap()
|
||||
|
||||
if (postData != null) {
|
||||
val data = ReadableMapUtils.toMap(postData)
|
||||
results.putMap("posts", postData)
|
||||
if (data != null) {
|
||||
val postsMap = data["posts"]
|
||||
if (postsMap != null) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val posts = ReadableMapUtils.toWritableMap(postsMap as? Map<String, Any>)
|
||||
val iterator = posts.keySetIterator()
|
||||
val userIds = mutableListOf<String>()
|
||||
val usernames = mutableListOf<String>()
|
||||
|
||||
val threads = WritableNativeArray()
|
||||
val threadParticipantUserIds = mutableListOf<String>() // Used to exclude the "userIds" present in the thread participants
|
||||
val threadParticipantUsernames = mutableListOf<String>() // Used to exclude the "usernames" present in the thread participants
|
||||
val threadParticipantUsers = HashMap<String, ReadableMap>() // All unique users from thread participants are stored here
|
||||
val userIdsAlreadyLoaded = mutableListOf<String>()
|
||||
if (loadedProfiles != null) {
|
||||
for (i in 0 until loadedProfiles.size()) {
|
||||
loadedProfiles.getMap(i).getString("id")?.let { userIdsAlreadyLoaded.add(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun findNeededUsernames(text: String?) {
|
||||
if (text == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val matchResults = regex.findAll(text)
|
||||
matchResults.iterator().forEach {
|
||||
val username = it.value.removePrefix("@")
|
||||
if (!usernames.contains(username) && currentUsername != username && !specialMentions.contains(username)) {
|
||||
usernames.add(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (iterator.hasNextKey()) {
|
||||
val key = iterator.nextKey()
|
||||
val post = posts.getMap(key)
|
||||
val userId = post?.getString("user_id")
|
||||
if (userId != null && userId != currentUserId && !userIdsAlreadyLoaded.contains(userId) && !userIds.contains(userId)) {
|
||||
userIds.add(userId)
|
||||
}
|
||||
|
||||
val message = post?.getString("message")
|
||||
findNeededUsernames(message)
|
||||
val props = post?.getMap("props")
|
||||
val attachments = props?.getArray("attachments")
|
||||
if (attachments != null) {
|
||||
for (i in 0 until attachments.size()) {
|
||||
val attachment = attachments.getMap(i)
|
||||
val pretext = attachment.getString("pretext")
|
||||
val text = attachment.getString("text")
|
||||
findNeededUsernames(pretext)
|
||||
findNeededUsernames(text)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (isCRTEnabled) {
|
||||
// Add root post as a thread
|
||||
val threadId = post?.getString("root_id")
|
||||
if (threadId.isNullOrEmpty()) {
|
||||
post?.let {
|
||||
val thread = Arguments.createMap()
|
||||
thread.putString("id", it.getString("id"))
|
||||
thread.putInt("reply_count", it.getInt("reply_count"))
|
||||
thread.putDouble("last_reply_at", 0.0)
|
||||
thread.putDouble("last_viewed_at", 0.0)
|
||||
thread.putArray("participants", it.getArray("participants"))
|
||||
thread.putMap("post", it)
|
||||
thread.putBoolean("is_following", try {
|
||||
it.getBoolean("is_following")
|
||||
} catch (e: NoSuchKeyException) {
|
||||
false
|
||||
})
|
||||
thread.putInt("unread_replies", 0)
|
||||
thread.putInt("unread_mentions", 0)
|
||||
thread.putDouble("delete_at", it.getDouble("delete_at"))
|
||||
threads.pushMap(thread)
|
||||
}
|
||||
}
|
||||
|
||||
// Add participant userIds and usernames to exclude them from getting fetched again
|
||||
val participants = post?.getArray("participants")
|
||||
participants?.let {
|
||||
for (i in 0 until it.size()) {
|
||||
val participant = it.getMap(i)
|
||||
|
||||
val participantId = participant.getString("id")
|
||||
if (participantId != currentUserId && participantId != null) {
|
||||
if (!threadParticipantUserIds.contains(participantId) && !userIdsAlreadyLoaded.contains(participantId)) {
|
||||
threadParticipantUserIds.add(participantId)
|
||||
}
|
||||
|
||||
if (!threadParticipantUsers.containsKey(participantId)) {
|
||||
threadParticipantUsers[participantId] = participant
|
||||
}
|
||||
}
|
||||
|
||||
val username = participant.getString("username")
|
||||
if (username != null && username != currentUsername && !threadParticipantUsernames.contains(username)) {
|
||||
threadParticipantUsernames.add(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val existingUserIds = queryIds(db, "User", userIds.toTypedArray())
|
||||
val existingUsernames = queryByColumn(db, "User", "username", usernames.toTypedArray())
|
||||
userIds.removeAll { it in existingUserIds }
|
||||
usernames.removeAll { it in existingUsernames }
|
||||
|
||||
if (threadParticipantUserIds.size > 0) {
|
||||
// Do not fetch users found in thread participants as we get the user's data in the posts response already
|
||||
userIds.removeAll { it in threadParticipantUserIds }
|
||||
usernames.removeAll { it in threadParticipantUsernames }
|
||||
|
||||
// Get users from thread participants
|
||||
val existingThreadParticipantUserIds = queryIds(db, "User", threadParticipantUserIds.toTypedArray())
|
||||
|
||||
// Exclude the thread participants already present in the DB from getting inserted again
|
||||
val usersFromThreads = WritableNativeArray()
|
||||
threadParticipantUsers.forEach { (userId, user) ->
|
||||
if (!existingThreadParticipantUserIds.contains(userId)) {
|
||||
usersFromThreads.pushMap(user)
|
||||
}
|
||||
}
|
||||
|
||||
if (usersFromThreads.size() > 0) {
|
||||
results.putArray("usersFromThreads", usersFromThreads)
|
||||
}
|
||||
}
|
||||
|
||||
if (userIds.size > 0) {
|
||||
results.putArray("userIdsToLoad", ReadableArrayUtils.toWritableArray(userIds.toTypedArray()))
|
||||
}
|
||||
|
||||
if (usernames.size > 0) {
|
||||
results.putArray("usernamesToLoad", ReadableArrayUtils.toWritableArray(usernames.toTypedArray()))
|
||||
}
|
||||
|
||||
if (threads.size() > 0) {
|
||||
results.putArray("threads", threads)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
results
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.database_extension.findMyTeam
|
||||
import com.mattermost.helpers.database_extension.findTeam
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
|
||||
suspend fun PushNotificationDataRunnable.Companion.fetchTeamIfNeeded(db: WMDatabase, serverUrl: String, teamId: String): Pair<ReadableMap?, ReadableMap?> {
|
||||
return try {
|
||||
var team: ReadableMap? = null
|
||||
var myTeam: ReadableMap? = null
|
||||
val teamExists = findTeam(db, teamId)
|
||||
val myTeamExists = findMyTeam(db, teamId)
|
||||
if (!teamExists) {
|
||||
team = fetch(serverUrl, "/api/v4/teams/$teamId")
|
||||
}
|
||||
|
||||
if (!myTeamExists) {
|
||||
myTeam = fetch(serverUrl, "/api/v4/teams/$teamId/members/me")
|
||||
}
|
||||
|
||||
Pair(team, myTeam)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Pair(null, null)
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.database_extension.*
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchThread(db: WMDatabase, serverUrl: String, threadId: String, teamId: String?): ReadableMap? {
|
||||
val currentUserId = queryCurrentUserId(db) ?: return null
|
||||
val threadTeamId = (if (teamId.isNullOrEmpty()) queryCurrentTeamId(db) else teamId) ?: return null
|
||||
|
||||
return try {
|
||||
val thread = fetch(serverUrl, "/api/v4/users/$currentUserId/teams/${threadTeamId}/threads/$threadId")
|
||||
thread?.getMap("data")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.ReadableArrayUtils
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchUsersById(serverUrl: String, userIds: ReadableArray): ReadableArray? {
|
||||
return try {
|
||||
val endpoint = "api/v4/users/ids"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(userIds)))
|
||||
val result = fetchWithPost(serverUrl, endpoint, options)
|
||||
result?.getArray("data")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchUsersByUsernames(serverUrl: String, usernames: ReadableArray): ReadableArray? {
|
||||
return try {
|
||||
val endpoint = "api/v4/users/usernames"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(usernames)))
|
||||
val result = fetchWithPost(serverUrl, endpoint, options)
|
||||
result?.getArray("data")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchNeededUsers(serverUrl: String, loadedUsers: ReadableArray?, data: ReadableMap?): ArrayList<Any> {
|
||||
val userList = ArrayList<Any>()
|
||||
loadedUsers?.let { PushNotificationDataRunnable.addUsersToList(it, userList) }
|
||||
data?.getArray("userIdsToLoad")?.let { ids ->
|
||||
if (ids.size() > 0) {
|
||||
val result = fetchUsersById(serverUrl, ids)
|
||||
result?.let { PushNotificationDataRunnable.addUsersToList(it, userList) }
|
||||
}
|
||||
}
|
||||
|
||||
data?.getArray("usernamesToLoad")?.let { ids ->
|
||||
if (ids.size() > 0) {
|
||||
val result = fetchUsersByUsernames(serverUrl, ids)
|
||||
result?.let { PushNotificationDataRunnable.addUsersToList(it, userList) }
|
||||
}
|
||||
}
|
||||
|
||||
data?.getArray("usersFromThreads")?.let { PushNotificationDataRunnable.addUsersToList(it, userList) }
|
||||
|
||||
return userList
|
||||
}
|
||||
|
||||
internal fun PushNotificationDataRunnable.Companion.addUsersToList(users: ReadableArray, list: ArrayList<Any>) {
|
||||
for (i in 0 until users.size()) {
|
||||
list.add(users.getMap(i))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import com.mattermost.helpers.CustomPushNotificationHelper;
|
||||
import com.mattermost.helpers.DatabaseHelper;
|
||||
import com.mattermost.helpers.Network;
|
||||
import com.mattermost.helpers.NotificationHelper;
|
||||
import com.mattermost.helpers.PushNotificationDataHelper;
|
||||
import com.mattermost.share.ShareModule;
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
import com.wix.reactnativenotifications.core.notification.PushNotification;
|
||||
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;
|
||||
|
||||
public class CustomPushNotification extends PushNotification {
|
||||
private final PushNotificationDataHelper dataHelper;
|
||||
|
||||
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
|
||||
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
|
||||
dataHelper = new PushNotificationDataHelper(context);
|
||||
|
||||
try {
|
||||
Objects.requireNonNull(DatabaseHelper.Companion.getInstance()).init(context);
|
||||
Network.init(context);
|
||||
NotificationHelper.cleanNotificationPreferencesIfNeeded(context);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceived() {
|
||||
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 = NotificationHelper.getNotificationId(initialData);
|
||||
|
||||
String serverUrl = addServerUrlToBundle(initialData);
|
||||
boolean isReactInit = mAppLifecycleFacade.isReactInitialized();
|
||||
|
||||
if (ackId != null && serverUrl != null) {
|
||||
Bundle response = ReceiptDelivery.send(ackId, serverUrl, postId, type, isIdLoaded);
|
||||
if (isIdLoaded && response != null) {
|
||||
Bundle current = mNotificationProps.asBundle();
|
||||
if (!current.containsKey("server_url")) {
|
||||
response.putString("server_url", serverUrl);
|
||||
}
|
||||
current.putAll(response);
|
||||
mNotificationProps = createProps(current);
|
||||
}
|
||||
}
|
||||
|
||||
finishProcessingNotification(serverUrl, type, channelId, notificationId, isReactInit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpened() {
|
||||
if (mNotificationProps != null) {
|
||||
digestNotification();
|
||||
|
||||
Bundle data = mNotificationProps.asBundle();
|
||||
NotificationHelper.clearChannelOrThreadNotifications(mContext, data);
|
||||
}
|
||||
}
|
||||
|
||||
private void finishProcessingNotification(String serverUrl, String type, String channelId, int notificationId, Boolean isReactInit) {
|
||||
switch (type) {
|
||||
case CustomPushNotificationHelper.PUSH_TYPE_MESSAGE:
|
||||
case CustomPushNotificationHelper.PUSH_TYPE_SESSION:
|
||||
ShareModule shareModule = ShareModule.getInstance();
|
||||
String currentActivityName = shareModule != null ? shareModule.getCurrentActivityName() : "";
|
||||
Log.i("ReactNative", currentActivityName);
|
||||
if (!mAppLifecycleFacade.isAppVisible() || !currentActivityName.equals("MainActivity")) {
|
||||
boolean createSummary = type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE);
|
||||
if (type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE)) {
|
||||
if (channelId != null) {
|
||||
Bundle notificationBundle = mNotificationProps.asBundle();
|
||||
if (serverUrl != null && !isReactInit) {
|
||||
// We will only fetch the data related to the notification on the native side
|
||||
// as updating the data directly to the db removes the wal & shm files needed
|
||||
// by watermelonDB, if the DB is updated while WDB is running it causes WDB to
|
||||
// detect the database as malformed, thus the app stop working and a restart is required.
|
||||
// Data will be fetch from within the JS context instead.
|
||||
dataHelper.fetchAndStoreDataForPushNotification(notificationBundle);
|
||||
}
|
||||
createSummary = NotificationHelper.addNotificationToPreferences(
|
||||
mContext,
|
||||
notificationId,
|
||||
notificationBundle
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
buildNotification(notificationId, createSummary);
|
||||
}
|
||||
break;
|
||||
case CustomPushNotificationHelper.PUSH_TYPE_CLEAR:
|
||||
NotificationHelper.clearChannelOrThreadNotifications(mContext, mNotificationProps.asBundle());
|
||||
break;
|
||||
}
|
||||
|
||||
if (isReactInit) {
|
||||
notifyReceivedToJS();
|
||||
}
|
||||
}
|
||||
|
||||
private void buildNotification(Integer notificationId, boolean createSummary) {
|
||||
final PendingIntent pendingIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, mNotificationProps);
|
||||
final Notification notification = buildNotification(pendingIntent);
|
||||
if (createSummary) {
|
||||
final Notification summary = getNotificationSummaryBuilder(pendingIntent).build();
|
||||
super.postNotification(summary, notificationId + 1);
|
||||
}
|
||||
super.postNotification(notification, notificationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NotificationCompat.Builder getNotificationBuilder(PendingIntent intent) {
|
||||
Bundle bundle = mNotificationProps.asBundle();
|
||||
return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, false);
|
||||
}
|
||||
|
||||
protected NotificationCompat.Builder getNotificationSummaryBuilder(PendingIntent intent) {
|
||||
Bundle bundle = mNotificationProps.asBundle();
|
||||
return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, true);
|
||||
}
|
||||
|
||||
private void notifyReceivedToJS() {
|
||||
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
|
||||
}
|
||||
|
||||
private String addServerUrlToBundle(Bundle bundle) {
|
||||
String serverId = bundle.getString("server_id");
|
||||
String serverUrl;
|
||||
if (serverId == null) {
|
||||
serverUrl = Objects.requireNonNull(DatabaseHelper.Companion.getInstance()).getOnlyServerUrl();
|
||||
} else {
|
||||
serverUrl = Objects.requireNonNull(DatabaseHelper.Companion.getInstance()).getServerUrlForIdentifier(serverId);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(serverUrl)) {
|
||||
bundle.putString("server_url", serverUrl);
|
||||
mNotificationProps = createProps(bundle);
|
||||
}
|
||||
|
||||
return serverUrl;
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
package com.mattermost.rnbeta
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.mattermost.helpers.CustomPushNotificationHelper
|
||||
import com.mattermost.helpers.DatabaseHelper
|
||||
import com.mattermost.helpers.Network
|
||||
import com.mattermost.helpers.PushNotificationDataHelper
|
||||
import com.mattermost.helpers.database_extension.getServerUrlForIdentifier
|
||||
import com.mattermost.rnutils.helpers.NotificationHelper
|
||||
import com.mattermost.turbolog.TurboLog
|
||||
import com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper
|
||||
import com.wix.reactnativenotifications.core.AppLifecycleFacade
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter
|
||||
import com.wix.reactnativenotifications.core.notification.PushNotification
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CustomPushNotification(
|
||||
context: Context,
|
||||
bundle: Bundle,
|
||||
appLifecycleFacade: AppLifecycleFacade,
|
||||
appLaunchHelper: AppLaunchHelper,
|
||||
jsIoHelper: JsIOHelper
|
||||
) : PushNotification(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper) {
|
||||
private val dataHelper = PushNotificationDataHelper(context)
|
||||
|
||||
init {
|
||||
try {
|
||||
DatabaseHelper.instance?.init(context)
|
||||
NotificationHelper.cleanNotificationPreferencesIfNeeded(context)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onReceived() {
|
||||
val initialData = mNotificationProps.asBundle()
|
||||
val type = initialData.getString("type")
|
||||
val ackId = initialData.getString("ack_id")
|
||||
val postId = initialData.getString("post_id")
|
||||
val channelId = initialData.getString("channel_id")
|
||||
val signature = initialData.getString("signature")
|
||||
val isIdLoaded = initialData.getString("id_loaded") == "true"
|
||||
val notificationId = NotificationHelper.getNotificationId(initialData)
|
||||
val serverUrl = addServerUrlToBundle(initialData)
|
||||
Network.init(mContext)
|
||||
|
||||
GlobalScope.launch {
|
||||
try {
|
||||
handlePushNotificationInCoroutine(serverUrl, type, channelId, ackId, isIdLoaded, notificationId, postId, signature)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handlePushNotificationInCoroutine(
|
||||
serverUrl: String?,
|
||||
type: String?,
|
||||
channelId: String?,
|
||||
ackId: String?,
|
||||
isIdLoaded: Boolean,
|
||||
notificationId: Int,
|
||||
postId: String?,
|
||||
signature: String?
|
||||
) {
|
||||
if (ackId != null && serverUrl != null) {
|
||||
val response = ReceiptDelivery.send(ackId, serverUrl, postId, type, isIdLoaded)
|
||||
if (isIdLoaded && response != null) {
|
||||
val current = mNotificationProps.asBundle()
|
||||
if (!current.containsKey("server_url")) {
|
||||
response.putString("server_url", serverUrl)
|
||||
}
|
||||
current.putAll(response)
|
||||
mNotificationProps = createProps(current)
|
||||
}
|
||||
}
|
||||
|
||||
if (!CustomPushNotificationHelper.verifySignature(mContext, signature, serverUrl, ackId)) {
|
||||
TurboLog.i("Mattermost Notifications Signature verification", "Notification skipped because we could not verify it.")
|
||||
return
|
||||
}
|
||||
|
||||
finishProcessingNotification(serverUrl, type, channelId, notificationId)
|
||||
}
|
||||
|
||||
override fun onOpened() {
|
||||
mNotificationProps?.let {
|
||||
digestNotification()
|
||||
NotificationHelper.clearChannelOrThreadNotifications(mContext, it.asBundle())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun finishProcessingNotification(serverUrl: String?, type: String?, channelId: String?, notificationId: Int) {
|
||||
val isReactInit = mAppLifecycleFacade.isReactInitialized()
|
||||
|
||||
when (type) {
|
||||
CustomPushNotificationHelper.PUSH_TYPE_MESSAGE, CustomPushNotificationHelper.PUSH_TYPE_SESSION -> {
|
||||
val currentActivityName = mAppLifecycleFacade.runningReactContext?.currentActivity?.componentName?.className ?: ""
|
||||
TurboLog.i("ReactNative", currentActivityName)
|
||||
if (!mAppLifecycleFacade.isAppVisible() || !currentActivityName.contains("MainActivity")) {
|
||||
var createSummary = type == CustomPushNotificationHelper.PUSH_TYPE_MESSAGE
|
||||
if (type == CustomPushNotificationHelper.PUSH_TYPE_MESSAGE) {
|
||||
channelId?.let {
|
||||
val notificationBundle = mNotificationProps.asBundle()
|
||||
serverUrl?.let {
|
||||
val notificationResult = dataHelper.fetchAndStoreDataForPushNotification(notificationBundle, isReactInit)
|
||||
notificationResult?.let { result ->
|
||||
notificationBundle.putBundle("data", result)
|
||||
mNotificationProps = createProps(notificationBundle)
|
||||
}
|
||||
}
|
||||
createSummary = NotificationHelper.addNotificationToPreferences(mContext, notificationId, notificationBundle)
|
||||
}
|
||||
}
|
||||
buildNotification(notificationId, createSummary)
|
||||
}
|
||||
}
|
||||
CustomPushNotificationHelper.PUSH_TYPE_CLEAR -> NotificationHelper.clearChannelOrThreadNotifications(mContext, mNotificationProps.asBundle())
|
||||
}
|
||||
|
||||
if (isReactInit) {
|
||||
notifyReceivedToJS()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotification(notificationId: Int, createSummary: Boolean) {
|
||||
val pendingIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, mNotificationProps)
|
||||
val notification = buildNotification(pendingIntent)
|
||||
if (createSummary) {
|
||||
val summary = getNotificationSummaryBuilder(pendingIntent).build()
|
||||
super.postNotification(summary, notificationId + 1)
|
||||
}
|
||||
super.postNotification(notification, notificationId)
|
||||
}
|
||||
|
||||
override fun getNotificationBuilder(intent: PendingIntent): NotificationCompat.Builder {
|
||||
val bundle = mNotificationProps.asBundle()
|
||||
return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, false)
|
||||
}
|
||||
|
||||
private fun getNotificationSummaryBuilder(intent: PendingIntent): NotificationCompat.Builder {
|
||||
val bundle = mNotificationProps.asBundle()
|
||||
return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, true)
|
||||
}
|
||||
|
||||
private fun notifyReceivedToJS() {
|
||||
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.runningReactContext)
|
||||
}
|
||||
|
||||
private fun addServerUrlToBundle(bundle: Bundle): String? {
|
||||
val dbHelper = DatabaseHelper.instance
|
||||
val serverId = bundle.getString("server_id")
|
||||
var serverUrl: String? = null
|
||||
|
||||
dbHelper?.let {
|
||||
serverUrl = if (serverId == null) {
|
||||
it.onlyServerUrl
|
||||
} else {
|
||||
it.getServerUrlForIdentifier(serverId)
|
||||
}
|
||||
|
||||
if (!serverUrl.isNullOrEmpty()) {
|
||||
bundle.putString("server_url", serverUrl)
|
||||
mNotificationProps = createProps(bundle)
|
||||
}
|
||||
}
|
||||
return serverUrl
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.mattermost.rnbeta
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.window.layout.FoldingFeature
|
||||
import androidx.window.layout.WindowInfoTracker
|
||||
import androidx.window.layout.WindowLayoutInfo
|
||||
import androidx.window.rxjava3.layout.windowLayoutInfoObservable
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
|
||||
class FoldableObserver(private val activity: Activity) {
|
||||
private var disposable: Disposable? = null
|
||||
private lateinit var observable: Observable<WindowLayoutInfo>
|
||||
|
||||
public fun onCreate() {
|
||||
observable = WindowInfoTracker.getOrCreate(activity)
|
||||
.windowLayoutInfoObservable(activity)
|
||||
}
|
||||
|
||||
public fun onStart() {
|
||||
if (disposable?.isDisposed == true) {
|
||||
onCreate()
|
||||
}
|
||||
disposable = observable.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { layoutInfo ->
|
||||
val splitViewModule = SplitViewModule.getInstance()
|
||||
val foldingFeature = layoutInfo.displayFeatures
|
||||
.filterIsInstance<FoldingFeature>()
|
||||
.firstOrNull()
|
||||
when {
|
||||
foldingFeature?.state === FoldingFeature.State.FLAT ->
|
||||
splitViewModule?.setDeviceFolded(false)
|
||||
isTableTopPosture(foldingFeature) ->
|
||||
splitViewModule?.setDeviceFolded(false)
|
||||
isBookPosture(foldingFeature) ->
|
||||
splitViewModule?.setDeviceFolded(false)
|
||||
else -> {
|
||||
splitViewModule?.setDeviceFolded(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun onStop() {
|
||||
disposable?.dispose()
|
||||
}
|
||||
|
||||
private fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
|
||||
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
|
||||
foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
|
||||
}
|
||||
|
||||
private fun isBookPosture(foldFeature : FoldingFeature?) : Boolean {
|
||||
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
|
||||
foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
import android.content.res.Configuration;
|
||||
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.reactnativenavigation.NavigationActivity;
|
||||
import com.github.emilioicai.hwkeyboardevent.HWKeyboardEventModule;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate;
|
||||
|
||||
public class MainActivity extends NavigationActivity {
|
||||
private boolean HWKeyboardConnected = false;
|
||||
private FoldableObserver foldableObserver = new FoldableObserver(this);
|
||||
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "Mattermost";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
|
||||
* DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
|
||||
* (aka React 18) with two boolean flags.
|
||||
*/
|
||||
@Override
|
||||
protected ReactActivityDelegate createReactActivityDelegate() {
|
||||
return new DefaultReactActivityDelegate(
|
||||
this,
|
||||
getMainComponentName(),
|
||||
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
|
||||
DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled
|
||||
// If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18).
|
||||
DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(null);
|
||||
setContentView(R.layout.launch_screen);
|
||||
setHWKeyboardConnected();
|
||||
foldableObserver.onCreate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
foldableObserver.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
foldableObserver.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) {
|
||||
HWKeyboardConnected = true;
|
||||
} else if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) {
|
||||
HWKeyboardConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean hasFocus) {
|
||||
super.onWindowFocusChanged(hasFocus);
|
||||
getReactGateway().onWindowFocusChanged(hasFocus);
|
||||
}
|
||||
|
||||
/*
|
||||
https://mattermost.atlassian.net/browse/MM-10601
|
||||
Required by react-native-hw-keyboard-event
|
||||
(https://github.com/emilioicai/react-native-hw-keyboard-event)
|
||||
*/
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (HWKeyboardConnected) {
|
||||
int keyCode = event.getKeyCode();
|
||||
int keyAction = event.getAction();
|
||||
if (keyAction == KeyEvent.ACTION_UP) {
|
||||
if (keyCode == KeyEvent.KEYCODE_ENTER) {
|
||||
String keyPressed = event.isShiftPressed() ? "shift-enter" : "enter";
|
||||
HWKeyboardEventModule.getInstance().keyPressed(keyPressed);
|
||||
return true;
|
||||
} else if (keyCode == KeyEvent.KEYCODE_K && event.isCtrlPressed()) {
|
||||
HWKeyboardEventModule.getInstance().keyPressed("find-channels");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
};
|
||||
|
||||
private void setHWKeyboardConnected() {
|
||||
HWKeyboardConnected = getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY;
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package com.mattermost.rnbeta
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
import com.mattermost.hardware.keyboard.MattermostHardwareKeyboardImpl
|
||||
import com.mattermost.rnutils.helpers.FoldableObserver
|
||||
import com.reactnativenavigation.NavigationActivity
|
||||
import expo.modules.ReactActivityDelegateWrapper
|
||||
|
||||
class MainActivity : NavigationActivity() {
|
||||
private var HWKeyboardConnected = false
|
||||
private val foldableObserver = FoldableObserver.getInstance(this)
|
||||
private var lastOrientation: Int = Configuration.ORIENTATION_UNDEFINED
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||
* rendering of the component.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "Mattermost"
|
||||
|
||||
/**
|
||||
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate =
|
||||
ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
|
||||
DefaultReactActivityDelegate(this, mainComponentName, DefaultNewArchitectureEntryPoint.fabricEnabled))
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(null)
|
||||
setContentView(R.layout.launch_screen)
|
||||
setHWKeyboardConnected()
|
||||
lastOrientation = this.resources.configuration.orientation
|
||||
foldableObserver.onCreate()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
foldableObserver.onStart()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
foldableObserver.onStop()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
foldableObserver.onDestroy()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
val newOrientation = newConfig.orientation
|
||||
if (newOrientation != lastOrientation) {
|
||||
lastOrientation = newOrientation
|
||||
foldableObserver.handleWindowLayoutInfo()
|
||||
}
|
||||
if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) {
|
||||
HWKeyboardConnected = true
|
||||
} else if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) {
|
||||
HWKeyboardConnected = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
reactGateway.onWindowFocusChanged(hasFocus)
|
||||
}
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
if (HWKeyboardConnected) {
|
||||
val ok = MattermostHardwareKeyboardImpl.dispatchKeyEvent(event)
|
||||
if (ok) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
private fun setHWKeyboardConnected() {
|
||||
HWKeyboardConnected = getResources().configuration.keyboard == Configuration.KEYBOARD_QWERTY
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.mattermost.helpers.RealPathUtil;
|
||||
import com.mattermost.share.ShareModule;
|
||||
import com.wix.reactnativenotifications.RNNotificationsPackage;
|
||||
|
||||
import com.reactnativenavigation.NavigationApplication;
|
||||
import com.wix.reactnativenotifications.core.notification.INotificationsApplication;
|
||||
import com.wix.reactnativenotifications.core.notification.IPushNotification;
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||
|
||||
import com.facebook.react.PackageList;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.TurboReactPackage;
|
||||
import com.facebook.react.bridge.JSIModuleSpec;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.JSIModulePackage;
|
||||
import com.facebook.react.module.model.ReactModuleInfo;
|
||||
import com.facebook.react.module.model.ReactModuleInfoProvider;
|
||||
import com.facebook.react.modules.network.OkHttpClientProvider;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import com.mattermost.flipper.ReactNativeFlipper;
|
||||
import com.mattermost.networkclient.RCTOkHttpClientFactory;
|
||||
import com.nozbe.watermelondb.jsi.WatermelonDBJSIPackage;
|
||||
|
||||
public class MainApplication extends NavigationApplication implements INotificationsApplication {
|
||||
public static MainApplication instance;
|
||||
|
||||
public Boolean sharedExtensionIsOpened = false;
|
||||
private final ReactNativeHost mReactNativeHost =
|
||||
new DefaultReactNativeHost(this) {
|
||||
@Override
|
||||
public boolean getUseDeveloperSupport() {
|
||||
return BuildConfig.DEBUG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ReactPackage> getPackages() {
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
// 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 TurboReactPackage() {
|
||||
@Override
|
||||
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
|
||||
switch (name) {
|
||||
case "MattermostManaged":
|
||||
return MattermostManagedModule.getInstance(reactContext);
|
||||
case "MattermostShare":
|
||||
return ShareModule.getInstance(reactContext);
|
||||
case "Notifications":
|
||||
return NotificationsModule.getInstance(instance, reactContext);
|
||||
case "SplitView":
|
||||
return SplitViewModule.Companion.getInstance(reactContext);
|
||||
default:
|
||||
throw new IllegalArgumentException("Could not find module " + name);
|
||||
}
|
||||
}
|
||||
|
||||
@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("Notifications", new ReactModuleInfo("Notifications", "com.mattermost.rnbeta.NotificationsModule", false, false, false, false, false));
|
||||
map.put("SplitView", new ReactModuleInfo("SplitView", "com.mattermost.rnbeta.SplitViewModule", false, false, false, false, false));
|
||||
return map;
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return packages;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JSIModulePackage getJSIModulePackage() {
|
||||
return (reactApplicationContext, jsContext) -> {
|
||||
List<JSIModuleSpec> modules = Collections.emptyList();
|
||||
modules.addAll(new WatermelonDBJSIPackage().getJSIModules(reactApplicationContext, jsContext));
|
||||
|
||||
return modules;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isNewArchEnabled() {
|
||||
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
|
||||
}
|
||||
@Override
|
||||
protected Boolean isHermesEnabled() {
|
||||
return BuildConfig.IS_HERMES_ENABLED;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public ReactNativeHost getReactNativeHost() {
|
||||
return mReactNativeHost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
instance = this;
|
||||
Context context = getApplicationContext();
|
||||
|
||||
// Delete any previous temp files created by the app
|
||||
File tempFolder = new File(context.getCacheDir(), RealPathUtil.CACHE_DIR_NAME);
|
||||
RealPathUtil.deleteTempFiles(tempFolder);
|
||||
Log.i("ReactNative", "Cleaning temp cache " + tempFolder.getAbsolutePath());
|
||||
|
||||
// Tells React Native to use our RCTOkHttpClientFactory which builds an OKHttpClient
|
||||
// with a cookie jar defined in APIClientModule and an interceptor to intercept all
|
||||
// requests that originate from React Native's OKHttpClient
|
||||
OkHttpClientProvider.setOkHttpClientFactory(new RCTOkHttpClientFactory());
|
||||
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
DefaultNewArchitectureEntryPoint.load();
|
||||
}
|
||||
ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPushNotification getPushNotification(Context context, Bundle bundle, AppLifecycleFacade defaultFacade, AppLaunchHelper defaultAppLaunchHelper) {
|
||||
return new CustomPushNotification(
|
||||
context,
|
||||
bundle,
|
||||
defaultFacade,
|
||||
defaultAppLaunchHelper,
|
||||
new JsIOHelper()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package com.mattermost.rnbeta
|
||||
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactHost
|
||||
import com.facebook.react.ReactNativeHost
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.bridge.UiThreadUtil
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||
import com.facebook.react.modules.network.OkHttpClientProvider
|
||||
import com.facebook.soloader.SoLoader
|
||||
import com.mattermost.networkclient.RCTOkHttpClientFactory
|
||||
import com.mattermost.rnshare.helpers.RealPathUtil
|
||||
import com.mattermost.turbolog.TurboLog
|
||||
import com.mattermost.turbolog.ConfigureOptions
|
||||
import com.nozbe.watermelondb.jsi.JSIInstaller
|
||||
import com.nozbe.watermelondb.jsi.WatermelonDBJSIPackage
|
||||
import com.reactnativenavigation.NavigationApplication
|
||||
import com.wix.reactnativenotifications.RNNotificationsPackage
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper
|
||||
import com.wix.reactnativenotifications.core.AppLifecycleFacade
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper
|
||||
import com.wix.reactnativenotifications.core.notification.INotificationsApplication
|
||||
import com.wix.reactnativenotifications.core.notification.IPushNotification
|
||||
import expo.modules.ApplicationLifecycleDispatcher
|
||||
import expo.modules.ReactNativeHostWrapper
|
||||
import expo.modules.image.okhttp.ExpoImageOkHttpClientGlideModule
|
||||
import java.io.File
|
||||
|
||||
class MainApplication : NavigationApplication(), INotificationsApplication {
|
||||
private var listenerAdded = false
|
||||
|
||||
override val reactNativeHost: ReactNativeHost =
|
||||
ReactNativeHostWrapper(this,
|
||||
object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> =
|
||||
PackageList(this).packages.apply {
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// add(MyReactNativePackage())
|
||||
add(RNNotificationsPackage(this@MainApplication))
|
||||
add(WatermelonDBJSIPackage())
|
||||
}
|
||||
|
||||
override fun getJSMainModuleName(): String = "index"
|
||||
|
||||
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
|
||||
|
||||
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
|
||||
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
|
||||
})
|
||||
|
||||
override val reactHost: ReactHost
|
||||
get() = getDefaultReactHost(applicationContext, reactNativeHost)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// Delete any previous temp files created by the app
|
||||
val tempFolder = File(applicationContext.cacheDir, RealPathUtil.CACHE_DIR_NAME)
|
||||
RealPathUtil.deleteTempFiles(tempFolder)
|
||||
TurboLog.configure(options = ConfigureOptions(logsDirectory = applicationContext.cacheDir.absolutePath + "/logs", logPrefix = applicationContext.packageName))
|
||||
|
||||
TurboLog.i("ReactNative", "Cleaning temp cache " + tempFolder.absolutePath)
|
||||
|
||||
// Tells React Native to use our RCTOkHttpClientFactory which builds an OKHttpClient
|
||||
// with a cookie jar defined in APIClientModule and an interceptor to intercept all
|
||||
// requests that originate from React Native's OKHttpClient
|
||||
|
||||
// Tells React Native to use our RCTOkHttpClientFactory which builds an OKHttpClient
|
||||
// with a cookie jar defined in APIClientModule and an interceptor to intercept all
|
||||
// requests that originate from React Native's OKHttpClient
|
||||
OkHttpClientProvider.setOkHttpClientFactory(RCTOkHttpClientFactory())
|
||||
ExpoImageOkHttpClientGlideModule.okHttpClient = RCTOkHttpClientFactory().createNewNetworkModuleClient()
|
||||
|
||||
SoLoader.init(this, OpenSourceMergedSoMapping)
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
load(bridgelessEnabled = false)
|
||||
}
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
|
||||
}
|
||||
|
||||
override fun getPushNotification(
|
||||
context: Context?,
|
||||
bundle: Bundle?,
|
||||
defaultFacade: AppLifecycleFacade?,
|
||||
defaultAppLaunchHelper: AppLaunchHelper?
|
||||
): IPushNotification {
|
||||
return CustomPushNotification(
|
||||
context!!,
|
||||
bundle!!,
|
||||
defaultFacade!!,
|
||||
defaultAppLaunchHelper!!,
|
||||
JsIOHelper()
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("VisibleForTests")
|
||||
private fun runOnJSQueueThread(action: () -> Unit) {
|
||||
reactNativeHost.reactInstanceManager.currentReactContext?.runOnJSQueueThread {
|
||||
action()
|
||||
} ?: UiThreadUtil.runOnUiThread {
|
||||
reactNativeHost.reactInstanceManager.currentReactContext?.runOnJSQueueThread {
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
import com.facebook.react.bridge.ActivityEventListener;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.BaseActivityEventListener;
|
||||
import com.facebook.react.bridge.GuardedResultAsyncTask;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
import com.mattermost.helpers.Credentials;
|
||||
import com.reactlibrary.createthumbnail.CreateThumbnailModule;
|
||||
import com.mattermost.helpers.RealPathUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URL;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
private static final String SAVE_EVENT = "MattermostManagedSaveFile";
|
||||
private static final Integer SAVE_REQUEST = 38641;
|
||||
private static MattermostManagedModule instance;
|
||||
private ReactApplicationContext reactContext;
|
||||
|
||||
private Promise mPickerPromise;
|
||||
private String fileContent;
|
||||
|
||||
private static final String TAG = MattermostManagedModule.class.getSimpleName();
|
||||
|
||||
private MattermostManagedModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.reactContext = reactContext;
|
||||
// Let the document provider know you're done by closing the stream.
|
||||
ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {
|
||||
@Override
|
||||
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) {
|
||||
if (requestCode == SAVE_REQUEST) {
|
||||
if (mPickerPromise != null) {
|
||||
if (resultCode == Activity.RESULT_CANCELED) {
|
||||
mPickerPromise.reject(SAVE_EVENT, "Save operation cancelled");
|
||||
} else if (resultCode == Activity.RESULT_OK) {
|
||||
Uri uri = intent.getData();
|
||||
if (uri == null) {
|
||||
mPickerPromise.reject(SAVE_EVENT, "No data found");
|
||||
} else {
|
||||
try {
|
||||
new SaveDataTask(reactContext, fileContent, uri).execute();
|
||||
mPickerPromise.resolve(uri.toString());
|
||||
} catch (Exception e) {
|
||||
mPickerPromise.reject(SAVE_EVENT, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mPickerPromise = null;
|
||||
} else if (resultCode == Activity.RESULT_OK) {
|
||||
try {
|
||||
Uri uri = intent.getData();
|
||||
if (uri != null)
|
||||
new SaveDataTask(reactContext, fileContent, uri).execute();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
reactContext.addActivityEventListener(mActivityEventListener);
|
||||
}
|
||||
|
||||
public static MattermostManagedModule getInstance(ReactApplicationContext reactContext) {
|
||||
if (instance == null) {
|
||||
instance = new MattermostManagedModule(reactContext);
|
||||
} else {
|
||||
instance.reactContext = reactContext;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static MattermostManagedModule getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void sendEvent(String eventName,
|
||||
@Nullable WritableMap params) {
|
||||
this.reactContext
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit(eventName, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String getName() {
|
||||
return "MattermostManaged";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getFilePath(String filePath, Promise promise) {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
WritableMap map = Arguments.createMap();
|
||||
|
||||
if (currentActivity != null) {
|
||||
Uri uri = Uri.parse(filePath);
|
||||
String path = RealPathUtil.getRealPathFromURI(currentActivity, uri);
|
||||
if (path != null) {
|
||||
String text = "file://" + path;
|
||||
map.putString("filePath", text);
|
||||
}
|
||||
}
|
||||
|
||||
promise.resolve(map);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void saveFile(String path, final Promise promise) {
|
||||
Uri contentUri;
|
||||
String filename = "";
|
||||
if(path.startsWith("content://")) {
|
||||
contentUri = Uri.parse(path);
|
||||
} else {
|
||||
File newFile = new File(path);
|
||||
filename = newFile.getName();
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
if(currentActivity == null) {
|
||||
promise.reject(SAVE_EVENT, "Activity doesn't exist");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final String packageName = currentActivity.getPackageName();
|
||||
final String authority = new StringBuilder(packageName).append(".provider").toString();
|
||||
contentUri = FileProvider.getUriForFile(currentActivity, authority, newFile);
|
||||
}
|
||||
catch(IllegalArgumentException e) {
|
||||
promise.reject(SAVE_EVENT, e.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(contentUri == null) {
|
||||
promise.reject(SAVE_EVENT, "Invalid file");
|
||||
return;
|
||||
}
|
||||
|
||||
String extension = MimeTypeMap.getFileExtensionFromUrl(path).toLowerCase();
|
||||
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
if (mimeType == null) {
|
||||
mimeType = RealPathUtil.getMimeType(path);
|
||||
}
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_CREATE_DOCUMENT);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType(mimeType);
|
||||
intent.putExtra(Intent.EXTRA_TITLE, filename);
|
||||
|
||||
PackageManager pm = getCurrentActivity().getPackageManager();
|
||||
if (intent.resolveActivity(pm) != null) {
|
||||
try {
|
||||
getCurrentActivity().startActivityForResult(intent, SAVE_REQUEST);
|
||||
mPickerPromise = promise;
|
||||
fileContent = path;
|
||||
}
|
||||
catch(Exception e) {
|
||||
promise.reject(SAVE_EVENT, e.getMessage());
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if(mimeType == null) {
|
||||
throw new Exception("It wasn't possible to detect the type of the file");
|
||||
}
|
||||
throw new Exception("No app associated with this mime type");
|
||||
}
|
||||
catch(Exception e) {
|
||||
promise.reject(SAVE_EVENT, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void createThumbnail(ReadableMap options, Promise promise) {
|
||||
try {
|
||||
WritableMap optionsMap = Arguments.createMap();
|
||||
optionsMap.merge(options);
|
||||
String url = options.hasKey("url") ? options.getString("url") : "";
|
||||
URL videoUrl = new URL(url);
|
||||
String serverUrl = videoUrl.getProtocol() + "://" + videoUrl.getHost() + ":" + videoUrl.getPort();
|
||||
String token = Credentials.getCredentialsForServerSync(this.reactContext, serverUrl);
|
||||
if (!TextUtils.isEmpty(token)) {
|
||||
WritableMap headers = Arguments.createMap();
|
||||
if (optionsMap.hasKey("headers")) {
|
||||
headers.merge(optionsMap.getMap("headers"));
|
||||
}
|
||||
headers.putString("Authorization", "Bearer " + token);
|
||||
optionsMap.putMap("headers", headers);
|
||||
}
|
||||
CreateThumbnailModule thumb = new CreateThumbnailModule(this.reactContext);
|
||||
thumb.create(optionsMap.copy(), promise);
|
||||
} catch (Exception e) {
|
||||
promise.reject("CreateThumbnail_ERROR", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SaveDataTask extends GuardedResultAsyncTask<Object> {
|
||||
private final WeakReference<Context> weakContext;
|
||||
private final String fromFile;
|
||||
private final Uri toFile;
|
||||
|
||||
protected SaveDataTask(ReactApplicationContext reactContext, String path, Uri destination) {
|
||||
super(reactContext.getExceptionHandler());
|
||||
weakContext = new WeakReference<>(reactContext.getApplicationContext());
|
||||
fromFile = path;
|
||||
toFile = destination;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object doInBackgroundGuarded() {
|
||||
FileChannel source = null;
|
||||
FileChannel dest = null;
|
||||
try {
|
||||
File input = new File(this.fromFile);
|
||||
FileInputStream fileInputStream = new FileInputStream(input);
|
||||
ParcelFileDescriptor pfd = weakContext.get().getContentResolver().openFileDescriptor(toFile, "w");
|
||||
FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());
|
||||
source = fileInputStream.getChannel();
|
||||
dest = fileOutputStream.getChannel();
|
||||
dest.transferFrom(source, 0, source.size());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (source != null) {
|
||||
try {
|
||||
source.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (dest != null) {
|
||||
try {
|
||||
dest.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecuteGuarded(Object o) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.app.IntentService;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import com.mattermost.rnutils.helpers.NotificationHelper;
|
||||
import com.mattermost.turbolog.TurboLog;
|
||||
import com.mattermost.helpers.NotificationHelper;
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
|
||||
public class NotificationDismissService extends IntentService {
|
||||
@@ -19,7 +19,7 @@ public class NotificationDismissService extends IntentService {
|
||||
final Context context = getApplicationContext();
|
||||
final Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
|
||||
|
||||
NotificationHelper.INSTANCE.dismissNotification(context, bundle);
|
||||
TurboLog.Companion.i("ReactNative", "Dismiss notification");
|
||||
NotificationHelper.dismissNotification(context, bundle);
|
||||
Log.i("ReactNative", "Dismiss notification");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,18 +8,16 @@ import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.Person;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
import com.mattermost.helpers.*;
|
||||
import com.mattermost.turbolog.TurboLog;
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
import com.wix.reactnativenotifications.core.notification.PushNotificationProps;
|
||||
|
||||
@@ -42,7 +40,6 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
|
||||
final int notificationId = intent.getIntExtra(CustomPushNotificationHelper.NOTIFICATION_ID, -1);
|
||||
final String serverUrl = bundle.getString("server_url");
|
||||
Network.init(context);
|
||||
if (serverUrl != null) {
|
||||
replyToMessage(serverUrl, notificationId, message);
|
||||
} else {
|
||||
@@ -69,6 +66,7 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
WritableMap headers = Arguments.createMap();
|
||||
headers.putString("Content-Type", "application/json");
|
||||
|
||||
|
||||
WritableMap body = Arguments.createMap();
|
||||
body.putString("channel_id", channelId);
|
||||
body.putString("message", message.toString());
|
||||
@@ -80,36 +78,26 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
|
||||
String postsEndpoint = "/api/v4/posts?set_online=false";
|
||||
Network.post(serverUrl, postsEndpoint, options, new ResolvePromise() {
|
||||
private boolean isSuccessful(int statusCode) {
|
||||
return statusCode >= 200 && statusCode < 300;
|
||||
}
|
||||
@Override
|
||||
public void resolve(@Nullable Object value) {
|
||||
if (value != null) {
|
||||
ReadableMap response = (ReadableMap)value;
|
||||
ReadableMap data = response.getMap("data");
|
||||
if (data != null && data.hasKey("status_code") && !isSuccessful(data.getInt("status_code"))) {
|
||||
TurboLog.Companion.i("ReactNative", String.format("Reply FAILED exception %s", data.getString("message")));
|
||||
onReplyFailed(notificationId);
|
||||
return;
|
||||
}
|
||||
onReplySuccess(notificationId, message);
|
||||
TurboLog.Companion.i("ReactNative", "Reply SUCCESS");
|
||||
Log.i("ReactNative", "Reply SUCCESS");
|
||||
} else {
|
||||
TurboLog.Companion.i("ReactNative", "Reply FAILED resolved without value");
|
||||
Log.i("ReactNative", "Reply FAILED resolved without value");
|
||||
onReplyFailed(notificationId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(@NonNull Throwable reason) {
|
||||
TurboLog.Companion.i("ReactNative", String.format("Reply FAILED exception %s", reason.getMessage()));
|
||||
public void reject(Throwable reason) {
|
||||
Log.i("ReactNative", String.format("Reply FAILED exception %s", reason.getMessage()));
|
||||
onReplyFailed(notificationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(@NonNull String code, String message) {
|
||||
TurboLog.Companion.i("ReactNative",
|
||||
public void reject(String code, String message) {
|
||||
Log.i("ReactNative",
|
||||
String.format("Reply FAILED status %s BODY %s", code, message)
|
||||
);
|
||||
onReplyFailed(notificationId);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.mattermost.helpers.NotificationHelper;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class NotificationsModule extends ReactContextBaseJavaModule {
|
||||
private static NotificationsModule instance;
|
||||
private final MainApplication mApplication;
|
||||
|
||||
private NotificationsModule(MainApplication application, ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
mApplication = application;
|
||||
}
|
||||
|
||||
public static NotificationsModule getInstance(MainApplication application, ReactApplicationContext reactContext) {
|
||||
if (instance == null) {
|
||||
instance = new NotificationsModule(application, reactContext);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Notifications";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getDeliveredNotifications(final Promise promise) {
|
||||
Context context = mApplication.getApplicationContext();
|
||||
StatusBarNotification[] notifications = NotificationHelper.getDeliveredNotifications(context);
|
||||
WritableArray result = Arguments.createArray();
|
||||
for (StatusBarNotification sbn:notifications) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
Notification n = sbn.getNotification();
|
||||
Bundle bundle = n.extras;
|
||||
Set<String> keys = bundle.keySet();
|
||||
for (String key: keys) {
|
||||
map.putString(key, bundle.getString(key));
|
||||
}
|
||||
result.pushMap(map);
|
||||
}
|
||||
promise.resolve(result);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeChannelNotifications(String serverUrl, String channelId) {
|
||||
Context context = mApplication.getApplicationContext();
|
||||
NotificationHelper.removeChannelNotifications(context, serverUrl, channelId);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeThreadNotifications(String serverUrl, String threadId) {
|
||||
Context context = mApplication.getApplicationContext();
|
||||
NotificationHelper.removeThreadNotifications(context, serverUrl, threadId);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeServerNotifications(String serverUrl) {
|
||||
Context context = mApplication.getApplicationContext();
|
||||
NotificationHelper.removeServerNotifications(context, serverUrl);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import java.lang.System;
|
||||
import java.util.Objects;
|
||||
@@ -11,7 +12,6 @@ import com.facebook.react.bridge.Arguments;
|
||||
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.mattermost.helpers.*;
|
||||
import com.mattermost.turbolog.TurboLog;
|
||||
|
||||
import okhttp3.Response;
|
||||
|
||||
@@ -19,7 +19,7 @@ public class ReceiptDelivery {
|
||||
private static final String[] ackKeys = new String[]{"post_id", "root_id", "category", "message", "team_id", "channel_id", "channel_name", "type", "sender_id", "sender_name", "version"};
|
||||
|
||||
public static Bundle send(final String ackId, final String serverUrl, final String postId, final String type, final boolean isIdLoaded) {
|
||||
TurboLog.Companion.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with ID-LOADED=%s", ackId, type, serverUrl, isIdLoaded));
|
||||
Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with ID-LOADED=%s", ackId, type, serverUrl, isIdLoaded));
|
||||
WritableMap options = Arguments.createMap();
|
||||
WritableMap headers = Arguments.createMap();
|
||||
WritableMap body = Arguments.createMap();
|
||||
@@ -38,7 +38,6 @@ public class ReceiptDelivery {
|
||||
JSONObject jsonResponse = new JSONObject(responseBody);
|
||||
return parseAckResponse(jsonResponse);
|
||||
} catch (Exception e) {
|
||||
TurboLog.Companion.e("ReactNative", "Send receipt delivery failed " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.mattermost.rnbeta
|
||||
|
||||
import com.facebook.react.bridge.*
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
|
||||
import com.learnium.RNDeviceInfo.resolver.DeviceTypeResolver
|
||||
|
||||
class SplitViewModule(private var reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
||||
private var isDeviceFolded: Boolean = false
|
||||
private var listenerCount = 0
|
||||
|
||||
companion object {
|
||||
private var instance: SplitViewModule? = null
|
||||
|
||||
fun getInstance(reactContext: ReactApplicationContext): SplitViewModule {
|
||||
if (instance == null) {
|
||||
instance = SplitViewModule(reactContext)
|
||||
} else {
|
||||
instance!!.reactContext = reactContext
|
||||
}
|
||||
|
||||
return instance!!
|
||||
}
|
||||
|
||||
fun getInstance(): SplitViewModule? {
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun getName() = "SplitView"
|
||||
|
||||
fun sendEvent(eventName: String,
|
||||
params: WritableMap?) {
|
||||
reactContext
|
||||
.getJSModule(RCTDeviceEventEmitter::class.java)
|
||||
.emit(eventName, params)
|
||||
}
|
||||
|
||||
private fun getSplitViewResults(folded: Boolean) : WritableMap? {
|
||||
if (currentActivity != null) {
|
||||
val deviceResolver = DeviceTypeResolver(this.reactContext)
|
||||
val map = Arguments.createMap()
|
||||
map.putBoolean("isSplitView", currentActivity!!.isInMultiWindowMode || folded)
|
||||
map.putBoolean("isTablet", deviceResolver.isTablet)
|
||||
return map
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun setDeviceFolded(folded: Boolean) {
|
||||
val map = getSplitViewResults(folded)
|
||||
if (listenerCount > 0 && isDeviceFolded != folded) {
|
||||
sendEvent("SplitViewChanged", map)
|
||||
}
|
||||
isDeviceFolded = folded
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun isRunningInSplitView(promise: Promise) {
|
||||
promise.resolve(getSplitViewResults(isDeviceFolded))
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun addListener(eventName: String) {
|
||||
listenerCount += 1
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun removeListeners(count: Int) {
|
||||
listenerCount -= count
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.mattermost.share;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.mattermost.rnbeta.MainApplication;
|
||||
|
||||
public class ShareActivity extends ReactActivity {
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "MattermostShare";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
MainApplication app = (MainApplication) this.getApplication();
|
||||
app.sharedExtensionIsOpened = true;
|
||||
}
|
||||
}
|
||||
258
android/app/src/main/java/com/mattermost/share/ShareModule.java
Normal file
258
android/app/src/main/java/com/mattermost/share/ShareModule.java
Normal file
@@ -0,0 +1,258 @@
|
||||
package com.mattermost.share;
|
||||
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.mattermost.helpers.Credentials;
|
||||
import com.mattermost.rnbeta.MainApplication;
|
||||
import com.mattermost.helpers.RealPathUtil;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class ShareModule extends ReactContextBaseJavaModule {
|
||||
private final OkHttpClient client = new OkHttpClient();
|
||||
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
private static ShareModule instance;
|
||||
private final MainApplication mApplication;
|
||||
private ReactApplicationContext mReactContext;
|
||||
private File tempFolder;
|
||||
|
||||
private ShareModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
mReactContext = reactContext;
|
||||
mApplication = (MainApplication)reactContext.getApplicationContext();
|
||||
}
|
||||
|
||||
public static ShareModule getInstance(ReactApplicationContext reactContext) {
|
||||
if (instance == null) {
|
||||
instance = new ShareModule(reactContext);
|
||||
} else {
|
||||
instance.mReactContext = reactContext;
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static ShareModule getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "MattermostShare";
|
||||
}
|
||||
|
||||
@ReactMethod(isBlockingSynchronousMethod = true)
|
||||
public String getCurrentActivityName() {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
if (currentActivity != null) {
|
||||
String actvName = currentActivity.getComponentName().getClassName();
|
||||
String[] components = actvName.split("\\.");
|
||||
return components[components.length - 1];
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void clear() {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
if (currentActivity != null && this.getCurrentActivityName().equals("ShareActivity")) {
|
||||
Intent intent = currentActivity.getIntent();
|
||||
intent.setAction("");
|
||||
intent.removeExtra(Intent.EXTRA_TEXT);
|
||||
intent.removeExtra(Intent.EXTRA_STREAM);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Map<String, Object> getConstants() {
|
||||
HashMap<String, Object> constants = new HashMap<>(1);
|
||||
constants.put("cacheDirName", RealPathUtil.CACHE_DIR_NAME);
|
||||
constants.put("isOpened", mApplication.sharedExtensionIsOpened);
|
||||
return constants;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void close(ReadableMap data) {
|
||||
this.clear();
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
if (currentActivity == null || !this.getCurrentActivityName().equals("ShareActivity")) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentActivity.finishAndRemoveTask();
|
||||
if (data != null && data.hasKey("serverUrl")) {
|
||||
ReadableArray files = data.getArray("files");
|
||||
String serverUrl = data.getString("serverUrl");
|
||||
final String token = Credentials.getCredentialsForServerSync(this.getReactApplicationContext(), serverUrl);
|
||||
JSONObject postData = buildPostObject(data);
|
||||
|
||||
if (files != null && files.size() > 0) {
|
||||
uploadFiles(serverUrl, token, files, postData);
|
||||
} else {
|
||||
try {
|
||||
post(serverUrl, token, postData);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mApplication.sharedExtensionIsOpened = false;
|
||||
RealPathUtil.deleteTempFiles(this.tempFolder);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getSharedData(Promise promise) {
|
||||
promise.resolve(processIntent());
|
||||
}
|
||||
|
||||
public WritableArray processIntent() {
|
||||
String type, action, extra;
|
||||
WritableArray items = Arguments.createArray();
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
|
||||
if (currentActivity != null) {
|
||||
this.tempFolder = new File(currentActivity.getCacheDir(), RealPathUtil.CACHE_DIR_NAME);
|
||||
Intent intent = currentActivity.getIntent();
|
||||
action = intent.getAction();
|
||||
type = intent.getType();
|
||||
extra = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
|
||||
if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type) && extra != null) {
|
||||
items.pushMap(ShareUtils.getTextItem(extra));
|
||||
} else if (Intent.ACTION_SEND.equals(action)) {
|
||||
if (extra != null) {
|
||||
items.pushMap(ShareUtils.getTextItem(extra));
|
||||
}
|
||||
Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
if (uri != null) {
|
||||
ReadableMap fileInfo = ShareUtils.getFileItem(currentActivity, uri);
|
||||
if (fileInfo != null) {
|
||||
items.pushMap(fileInfo);
|
||||
}
|
||||
}
|
||||
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
|
||||
if (extra != null) {
|
||||
items.pushMap(ShareUtils.getTextItem(extra));
|
||||
}
|
||||
|
||||
ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||
for (Uri uri : uris) {
|
||||
ReadableMap fileInfo = ShareUtils.getFileItem(currentActivity, uri);
|
||||
if (fileInfo != null) {
|
||||
items.pushMap(fileInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private JSONObject buildPostObject(ReadableMap data) {
|
||||
JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put("user_id", data.getString("userId"));
|
||||
if (data.hasKey("channelId")) {
|
||||
json.put("channel_id", data.getString("channelId"));
|
||||
}
|
||||
if (data.hasKey("message")) {
|
||||
json.put("message", data.getString("message"));
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
private void post(String serverUrl, String token, JSONObject postData) throws IOException {
|
||||
RequestBody body = RequestBody.create(postData.toString(), JSON);
|
||||
Request request = new Request.Builder()
|
||||
.header("Authorization", "BEARER " + token)
|
||||
.url(serverUrl + "/api/v4/posts")
|
||||
.post(body)
|
||||
.build();
|
||||
client.newCall(request).execute();
|
||||
}
|
||||
|
||||
private void uploadFiles(String serverUrl, String token, ReadableArray files, JSONObject postData) {
|
||||
try {
|
||||
MultipartBody.Builder builder = new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM);
|
||||
|
||||
for(int i = 0 ; i < files.size() ; i++) {
|
||||
ReadableMap file = files.getMap(i);
|
||||
String mime = file.getString("type");
|
||||
String fullPath = file.getString("value");
|
||||
if (fullPath != null) {
|
||||
String filePath = fullPath.replaceFirst("file://", "");
|
||||
File fileInfo = new File(filePath);
|
||||
if (fileInfo.exists() && mime != null) {
|
||||
final MediaType MEDIA_TYPE = MediaType.parse(mime);
|
||||
builder.addFormDataPart("files", file.getString("filename"), RequestBody.create(fileInfo, MEDIA_TYPE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.addFormDataPart("channel_id", postData.getString("channel_id"));
|
||||
RequestBody body = builder.build();
|
||||
Request request = new Request.Builder()
|
||||
.header("Authorization", "BEARER " + token)
|
||||
.url(serverUrl + "/api/v4/files")
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (response.isSuccessful()) {
|
||||
String responseData = response.body().string();
|
||||
JSONObject responseJson = new JSONObject(responseData);
|
||||
JSONArray fileInfoArray = responseJson.getJSONArray("file_infos");
|
||||
JSONArray file_ids = new JSONArray();
|
||||
for(int i = 0 ; i < fileInfoArray.length() ; i++) {
|
||||
JSONObject fileInfo = fileInfoArray.getJSONObject(i);
|
||||
file_ids.put(fileInfo.getString("id"));
|
||||
}
|
||||
postData.put("file_ids", file_ids);
|
||||
post(serverUrl, token, postData);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user