Compare commits
6 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b645c2bc2e | ||
|
|
46c0f59692 | ||
|
|
502d304101 | ||
|
|
48f31e753b | ||
|
|
9465cb199a | ||
|
|
bcbbc34f1e |
2810 changed files with 100174 additions and 310023 deletions
115
.eslintrc.json
Normal file
115
.eslintrc.json
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
{
|
||||
"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,
|
||||
"@typescript-eslint/no-unsafe-declaration-merging": "off",
|
||||
"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();
|
||||
|
|
@ -12,18 +12,14 @@ runs:
|
|||
- name: ci/prepare-mobile-build
|
||||
uses: ./.github/actions/prepare-mobile-build
|
||||
|
||||
- name: ci/apply-16kb-pagesize-patch
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::Apply 16KB page size compatibility patch"
|
||||
npm run apply-16kb-pagesize-patch
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: ci/setup-java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
# Disable this since we are not caching anything for now
|
||||
# - name: ci/install-gradle-dependencies
|
||||
# shell: bash
|
||||
# working-directory: android
|
||||
# run: |
|
||||
# echo "::group::install-gradle-dependencies"
|
||||
# ./gradlew dependencies
|
||||
# echo "::endgroup::"
|
||||
|
||||
- name: ci/jetify-android-libraries
|
||||
shell: bash
|
||||
|
|
@ -34,7 +30,7 @@ runs:
|
|||
|
||||
- name: ci/checkout-private-repo
|
||||
if: ${{ inputs.sign == 'true' }}
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
with:
|
||||
repository: mattermost/mattermost-mobile-private
|
||||
token: ${{ env.MATTERMOST_BUILD_GH_TOKEN }}
|
||||
|
|
|
|||
54
.github/actions/prepare-ios-build/action.yaml
vendored
54
.github/actions/prepare-ios-build/action.yaml
vendored
|
|
@ -1,61 +1,25 @@
|
|||
name: prepare-ios-build
|
||||
description: Action to prepare environment for ios build
|
||||
|
||||
inputs:
|
||||
intune-enabled:
|
||||
description: 'Enable Intune MAM features (requires Xcode 26.1+)'
|
||||
required: false
|
||||
default: 'false'
|
||||
intune-ssh-private-key:
|
||||
description: 'SSH private key for Intune submodule repository access (required if intune-enabled is true)'
|
||||
required: false
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: ci/setup-xcode
|
||||
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
|
||||
with:
|
||||
xcode-version: '26.1'
|
||||
- name: ci/install-os-deps
|
||||
env:
|
||||
HOMEBREW_NO_AUTO_UPDATE: "1"
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::install-os-deps"
|
||||
brew install watchman
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: ci/prepare-mobile-build
|
||||
uses: ./.github/actions/prepare-mobile-build
|
||||
|
||||
- name: ci/setup-intune
|
||||
if: inputs.intune-enabled == 'true'
|
||||
uses: ./.github/actions/setup-intune
|
||||
with:
|
||||
ssh-private-key: ${{ inputs.intune-ssh-private-key }}
|
||||
|
||||
- name: Get Intune submodule commit hash
|
||||
if: inputs.intune-enabled == 'true'
|
||||
id: intune-hash
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -e "libraries/@mattermost/intune/.git" ]; then
|
||||
INTUNE_HASH=$(cd libraries/@mattermost/intune && git rev-parse --short HEAD)
|
||||
echo "hash=${INTUNE_HASH}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "hash=none" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Cache Pods
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
with:
|
||||
path: |
|
||||
ios/Pods
|
||||
libraries/@mattermost/intune/ios/Frameworks
|
||||
key: ${{ runner.os }}-pods-v4-intune-${{ inputs.intune-enabled }}-${{ steps.intune-hash.outputs.hash }}-${{ hashFiles('ios/Podfile.lock') }}-${{ github.ref_name }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pods-v4-intune-${{ inputs.intune-enabled }}-${{ steps.intune-hash.outputs.hash }}-${{ hashFiles('ios/Podfile.lock') }}-
|
||||
|
||||
- name: ci/install-pods-dependencies
|
||||
shell: bash
|
||||
env:
|
||||
INTUNE_ENABLED: ${{ inputs.intune-enabled }}
|
||||
run: |
|
||||
echo "::group::install-pods-dependencies"
|
||||
echo "INTUNE_ENABLED=$INTUNE_ENABLED"
|
||||
echo "::group::install-pods-dependencies"
|
||||
npm run ios-gems
|
||||
npm run pod-install
|
||||
echo "::endgroup::"
|
||||
|
|
|
|||
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
|
||||
13
.github/actions/prepare-mobile-build/action.yaml
vendored
13
.github/actions/prepare-mobile-build/action.yaml
vendored
|
|
@ -4,8 +4,9 @@ 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
|
||||
- uses: ruby/setup-ruby@9669f3ee51dc3f4eda8447ab696b3ab19a90d14b # v1.144.0
|
||||
with:
|
||||
ruby-version: "2.7.7"
|
||||
|
||||
- name: ci/setup-fastlane-dependencies
|
||||
shell: bash
|
||||
|
|
@ -15,13 +16,5 @@ runs:
|
|||
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
|
||||
|
|
|
|||
10
.github/actions/prepare-node-deps/action.yaml
vendored
10
.github/actions/prepare-node-deps/action.yaml
vendored
|
|
@ -5,7 +5,7 @@ runs:
|
|||
using: composite
|
||||
steps:
|
||||
- name: ci/setup-node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "npm"
|
||||
|
|
@ -21,14 +21,6 @@ runs:
|
|||
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: |
|
||||
|
|
|
|||
27
.github/actions/setup-intune-ssh-key/action.yaml
vendored
27
.github/actions/setup-intune-ssh-key/action.yaml
vendored
|
|
@ -1,27 +0,0 @@
|
|||
# Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
# See LICENSE.txt for license information.
|
||||
|
||||
name: Setup Intune SSH Key
|
||||
description: Configure SSH private key for accessing Intune submodule repository
|
||||
|
||||
inputs:
|
||||
ssh-private-key:
|
||||
description: 'SSH private key for Intune submodule repository access'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Configure Intune submodule SSH key
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
|
||||
# Configure Intune submodule SSH key (separate from main repo key)
|
||||
INTUNE_KEY_PATH="${HOME}/.ssh/intune_submodule_ed25519"
|
||||
echo -e '${{ inputs.ssh-private-key }}' > ${INTUNE_KEY_PATH}
|
||||
chmod 0600 ${INTUNE_KEY_PATH}
|
||||
ssh-keygen -y -f ${INTUNE_KEY_PATH} > ${INTUNE_KEY_PATH}.pub
|
||||
|
||||
echo "✅ Intune submodule SSH key configured"
|
||||
60
.github/actions/setup-intune/action.yaml
vendored
60
.github/actions/setup-intune/action.yaml
vendored
|
|
@ -1,60 +0,0 @@
|
|||
# Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
# See LICENSE.txt for license information.
|
||||
|
||||
name: Setup Intune
|
||||
description: Initialize Intune submodule and link module for internal builds with MAM features
|
||||
|
||||
inputs:
|
||||
ssh-private-key:
|
||||
description: 'SSH private key for Intune submodule repository access'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Configure SSH for Intune submodule
|
||||
uses: ./.github/actions/setup-intune-ssh-key
|
||||
with:
|
||||
ssh-private-key: ${{ inputs.ssh-private-key }}
|
||||
|
||||
- name: Initialize Intune submodule
|
||||
shell: bash
|
||||
env:
|
||||
GIT_SSH_COMMAND: ssh -i $HOME/.ssh/intune_submodule_ed25519 -o IdentitiesOnly=yes -o StrictHostKeyChecking=no
|
||||
run: |
|
||||
echo "::group::Initialize Intune submodule"
|
||||
# Check if submodule is already initialized
|
||||
if [ -e "libraries/@mattermost/intune/.git" ]; then
|
||||
echo "ℹ️ Intune submodule already initialized, updating..."
|
||||
git submodule update --remote libraries/@mattermost/intune
|
||||
else
|
||||
echo "📥 Initializing Intune submodule..."
|
||||
git submodule update --init --recursive libraries/@mattermost/intune
|
||||
fi
|
||||
|
||||
# Display submodule info
|
||||
cd libraries/@mattermost/intune
|
||||
echo "📦 Intune submodule initialized:"
|
||||
echo " Branch: $(git rev-parse --abbrev-ref HEAD)"
|
||||
echo " Commit: $(git rev-parse --short HEAD)"
|
||||
echo " Remote: $(git remote get-url origin)"
|
||||
cd ../..
|
||||
|
||||
echo "✅ Intune submodule ready"
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: Link Intune module to node_modules
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::Link Intune module"
|
||||
# Install the module without saving to package.json
|
||||
npm install --install-links --no-save ./libraries/@mattermost/intune
|
||||
|
||||
# Verify symlink was created
|
||||
if [ -L "node_modules/@mattermost/intune" ] || [ -d "node_modules/@mattermost/intune" ]; then
|
||||
echo "✅ Intune module linked to node_modules/@mattermost/intune"
|
||||
else
|
||||
echo "❌ Failed to link Intune module"
|
||||
exit 1
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
23
.github/actions/setup-ssh-key/action.yaml
vendored
23
.github/actions/setup-ssh-key/action.yaml
vendored
|
|
@ -1,23 +0,0 @@
|
|||
# Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
# See LICENSE.txt for license information.
|
||||
|
||||
name: Setup SSH Key
|
||||
description: Configure SSH private key for accessing private repositories
|
||||
|
||||
inputs:
|
||||
ssh-private-key:
|
||||
description: 'SSH private key for repository access'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Configure SSH private key
|
||||
shell: bash
|
||||
run: |
|
||||
SSH_KEY_PATH=~/.ssh/id_ed25519
|
||||
mkdir -p ~/.ssh
|
||||
echo -e '${{ inputs.ssh-private-key }}' > ${SSH_KEY_PATH}
|
||||
chmod 0600 ${SSH_KEY_PATH}
|
||||
ssh-keygen -y -f ${SSH_KEY_PATH} > ${SSH_KEY_PATH}.pub
|
||||
echo "✅ SSH key configured"
|
||||
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 }}
|
||||
6
.github/actions/test/action.yaml
vendored
6
.github/actions/test/action.yaml
vendored
|
|
@ -13,16 +13,12 @@ runs:
|
|||
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
|
||||
npm test
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: ci/check-i18n
|
||||
shell: bash
|
||||
run: |
|
||||
|
|
|
|||
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"
|
||||
32
.github/workflows/build-android-beta.yml
vendored
32
.github/workflows/build-android-beta.yml
vendored
|
|
@ -5,10 +5,11 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- build-beta-[0-9]+
|
||||
- build-beta-android-[0-9]+
|
||||
- build-android-[0-9]+
|
||||
- build-android-beta-[0-9]+
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.14.0
|
||||
NODE_VERSION: 18.7.0
|
||||
TERM: xterm
|
||||
|
||||
jobs:
|
||||
|
|
@ -16,7 +17,7 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
||||
|
|
@ -24,12 +25,9 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- test
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
||||
- name: ci/prepare-android-build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
|
|
@ -57,25 +55,7 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-android-beta-build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: android-build-beta-${{ github.run_id }}
|
||||
path: "*.apk"
|
||||
|
||||
# New AWS bucket deployment steps
|
||||
- name: Configure AWS credentials new bucket
|
||||
id: configure-aws-credentials-new-bucket
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1
|
||||
with:
|
||||
aws-region: us-east-1
|
||||
role-to-assume: ${{ secrets.MM_MOBILE_RELEASE_AWS_ROLE_TO_ASSUME }}
|
||||
|
||||
- name: ci/upload-android-beta-new-bucket
|
||||
env:
|
||||
AWS_NEW_BUCKET_NAME: "${{ secrets.MM_MOBILE_RELEASE_AWS_NEW_BUCKET_NAME }}"
|
||||
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_BETA_MATTERMOST_WEBHOOK_URL }}"
|
||||
run: |
|
||||
echo "::group::Upload to new S3 bucket"
|
||||
bundle exec fastlane android upload_artifacts_to_s3 --env android.beta
|
||||
echo "::endgroup::"
|
||||
working-directory: ./fastlane
|
||||
|
|
|
|||
27
.github/workflows/build-android-release.yml
vendored
27
.github/workflows/build-android-release.yml
vendored
|
|
@ -8,7 +8,7 @@ on:
|
|||
- build-release-android-[0-9]+
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.14.0
|
||||
NODE_VERSION: 18.7.0
|
||||
TERM: xterm
|
||||
|
||||
jobs:
|
||||
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
||||
|
|
@ -24,12 +24,9 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- test
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
||||
- name: ci/prepare-android-build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
|
|
@ -57,23 +54,7 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-android-release-build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: android-build-release-${{ github.run_id }}
|
||||
path: "*.apk"
|
||||
|
||||
- name: Configure AWS credentials new bucket
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1
|
||||
with:
|
||||
aws-region: us-east-1
|
||||
role-to-assume: ${{ secrets.MM_MOBILE_RELEASE_AWS_ROLE_TO_ASSUME }}
|
||||
|
||||
- name: ci/upload-android-release-new-bucket
|
||||
env:
|
||||
AWS_NEW_BUCKET_NAME: "${{ secrets.MM_MOBILE_RELEASE_AWS_NEW_BUCKET_NAME }}"
|
||||
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_RELEASE_MATTERMOST_WEBHOOK_URL }}"
|
||||
run: |
|
||||
echo "::group::Upload to new S3 bucket"
|
||||
bundle exec fastlane android upload_artifacts_to_s3 --env android.release
|
||||
echo "::endgroup::"
|
||||
working-directory: ./fastlane
|
||||
|
|
|
|||
74
.github/workflows/build-ios-beta.yml
vendored
74
.github/workflows/build-ios-beta.yml
vendored
|
|
@ -9,36 +9,29 @@ on:
|
|||
- build-beta-sim-[0-9]+
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.14.0
|
||||
NODE_VERSION: 18.7.0
|
||||
TERM: xterm
|
||||
INTUNE_ENABLED: 'true'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
||||
build-ios-simulator:
|
||||
runs-on: macos-15-large
|
||||
runs-on: macos-12
|
||||
if: ${{ !contains(github.ref_name, 'beta-ios') }}
|
||||
needs:
|
||||
- test
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
||||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
||||
|
||||
- name: ci/build-ios-simulator
|
||||
env:
|
||||
|
|
@ -51,49 +44,31 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-ios-pr-simulator
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: ios-build-simulator-${{ github.run_id }}
|
||||
path: Mattermost-simulator-x86_64.app.zip
|
||||
|
||||
# Logic for new bucket upload
|
||||
- name: Configure AWS credentials new bucket
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1
|
||||
with:
|
||||
aws-region: us-east-1
|
||||
role-to-assume: ${{ secrets.MM_MOBILE_RELEASE_AWS_ROLE_TO_ASSUME }}
|
||||
|
||||
- name: ci/upload-ios-simulator-new-bucket
|
||||
env:
|
||||
TAG: "${{ github.ref_name }}"
|
||||
AWS_NEW_BUCKET_NAME: "${{ secrets.MM_MOBILE_RELEASE_AWS_NEW_BUCKET_NAME }}"
|
||||
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_BETA_MATTERMOST_WEBHOOK_URL }}"
|
||||
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
|
||||
run: bundle exec fastlane ios upload_artifacts_to_s3 --env ios.simulator output_file:"Mattermost-simulator-x86_64.app.zip"
|
||||
working-directory: ./fastlane
|
||||
|
||||
build-and-deploy-ios-beta:
|
||||
runs-on: macos-15-large
|
||||
runs-on: macos-12
|
||||
if: ${{ !contains(github.ref_name, 'beta-sim') }}
|
||||
needs:
|
||||
- test
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
||||
- name: ci/setup-ssh-key
|
||||
uses: ./.github/actions/setup-ssh-key
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}
|
||||
- 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
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
||||
|
||||
- name: ci/build-and-deploy-ios-beta
|
||||
env:
|
||||
|
|
@ -118,24 +93,7 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-ios-beta-build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: ios-build-beta-${{ github.run_id }}
|
||||
path: "*.ipa"
|
||||
|
||||
# Logic for new bucket upload
|
||||
- name: Configure AWS credentials new bucket
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1
|
||||
with:
|
||||
aws-region: us-east-1
|
||||
role-to-assume: ${{ secrets.MM_MOBILE_RELEASE_AWS_ROLE_TO_ASSUME }}
|
||||
|
||||
- name: ci/upload-ios-beta-new-bucket
|
||||
env:
|
||||
AWS_NEW_BUCKET_NAME: "${{ secrets.MM_MOBILE_RELEASE_AWS_NEW_BUCKET_NAME }}"
|
||||
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_BETA_MATTERMOST_WEBHOOK_URL }}"
|
||||
run: |
|
||||
echo "::group::Upload to new S3 bucket"
|
||||
bundle exec fastlane ios upload_artifacts_to_s3 --env ios.beta
|
||||
echo "::endgroup::"
|
||||
working-directory: ./fastlane
|
||||
|
|
|
|||
76
.github/workflows/build-ios-release.yml
vendored
76
.github/workflows/build-ios-release.yml
vendored
|
|
@ -9,47 +9,43 @@ on:
|
|||
- build-release-sim-[0-9]+
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.14.0
|
||||
NODE_VERSION: 18.7.0
|
||||
TERM: xterm
|
||||
INTUNE_ENABLED: 'true'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
||||
build-and-deploy-ios-release:
|
||||
runs-on: macos-15-large
|
||||
runs-on: macos-12
|
||||
if: ${{ !contains(github.ref_name, 'release-sim') }}
|
||||
needs:
|
||||
- test
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/setup-ssh-key
|
||||
uses: ./.github/actions/setup-ssh-key
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
||||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
||||
|
||||
- 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 }}"
|
||||
AWS_NEW_BUCKET_NAME: "${{ secrets.MM_MOBILE_RELEASE_AWS_NEW_BUCKET_NAME }}"
|
||||
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 }}"
|
||||
|
|
@ -69,45 +65,22 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-ios-release-build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: ios-build-release-${{ github.run_id }}
|
||||
path: "*.ipa"
|
||||
|
||||
# Upload artifacts to new bucket
|
||||
- name: Configure AWS credentials new bucket
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1
|
||||
with:
|
||||
aws-region: us-east-1
|
||||
role-to-assume: ${{ secrets.MM_MOBILE_RELEASE_AWS_ROLE_TO_ASSUME }}
|
||||
|
||||
- name: ci/upload-ios-release-to-new-bucket
|
||||
env:
|
||||
AWS_NEW_BUCKET_NAME: "${{ secrets.MM_MOBILE_RELEASE_AWS_NEW_BUCKET_NAME }}"
|
||||
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_RELEASE_MATTERMOST_WEBHOOK_URL }}"
|
||||
run: |
|
||||
echo "::group::Build"
|
||||
bundle exec fastlane ios upload_artifacts_to_s3 --env ios.release
|
||||
echo "::endgroup::"
|
||||
working-directory: ./fastlane
|
||||
|
||||
build-ios-simulator:
|
||||
runs-on: macos-15-large
|
||||
runs-on: macos-12
|
||||
if: ${{ !contains(github.ref_name , 'release-ios') }}
|
||||
needs:
|
||||
- test
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
||||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
||||
|
||||
- name: ci/build-ios-simulator
|
||||
env:
|
||||
|
|
@ -120,22 +93,7 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-ios-pr-simulator
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: ios-build-simulator-${{ github.run_id }}
|
||||
path: Mattermost-simulator-x86_64.app.zip
|
||||
|
||||
# Logic for new bucket upload
|
||||
- name: Configure AWS credentials new bucket
|
||||
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1
|
||||
with:
|
||||
aws-region: us-east-1
|
||||
role-to-assume: ${{ secrets.MM_MOBILE_RELEASE_AWS_ROLE_TO_ASSUME }}
|
||||
|
||||
- name: ci/upload-ios-simulator-new-bucket
|
||||
env:
|
||||
TAG: "${{ github.ref_name }}"
|
||||
AWS_NEW_BUCKET_NAME: "${{ secrets.MM_MOBILE_RELEASE_AWS_NEW_BUCKET_NAME }}"
|
||||
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_BETA_MATTERMOST_WEBHOOK_URL }}"
|
||||
run: bundle exec fastlane ios simulator --env ios.simulator output_file:"Mattermost-simulator-x86_64.app.zip"
|
||||
working-directory: ./fastlane
|
||||
|
|
|
|||
55
.github/workflows/build-pr.yml
vendored
55
.github/workflows/build-pr.yml
vendored
|
|
@ -1,52 +1,49 @@
|
|||
---
|
||||
name: build-pr
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
push:
|
||||
branches:
|
||||
- build-pr-*
|
||||
- build-pr-android-*
|
||||
- build-pr-ios-*
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.14.0
|
||||
NODE_VERSION: 18.7.0
|
||||
TERM: xterm
|
||||
INTUNE_ENABLED: 'true'
|
||||
|
||||
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 }}
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
||||
build-ios-pr:
|
||||
runs-on: macos-15-large
|
||||
if: ${{ github.event.label.name == 'Build Apps for PR' || github.event.label.name == 'Build App for iOS' }}
|
||||
runs-on: macos-12
|
||||
if: ${{ !contains(github.ref_name, '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/setup-ssh-key
|
||||
uses: ./.github/actions/setup-ssh-key
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
||||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
||||
|
||||
- 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 }}"
|
||||
BRANCH_TO_BUILD: "${{ github.ref_name }}"
|
||||
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 }}"
|
||||
|
|
@ -60,21 +57,19 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-ios-pr-build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.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' }}
|
||||
if: ${{ !contains(github.ref_name, 'ios') }}
|
||||
needs:
|
||||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
||||
- name: ci/prepare-android-build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
|
|
@ -86,7 +81,7 @@ jobs:
|
|||
|
||||
- name: ci/build-android-pr
|
||||
env:
|
||||
BRANCH_TO_BUILD: "${{ github.event.pull_request.head.ref }}"
|
||||
BRANCH_TO_BUILD: "${{ github.ref_name }}"
|
||||
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 }}"
|
||||
|
|
@ -94,7 +89,7 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-android-pr-build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
with:
|
||||
name: android-build-pr-${{ github.run_id }}
|
||||
path: "*.apk"
|
||||
|
|
|
|||
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
|
|
@ -1,17 +1,10 @@
|
|||
---
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'release*'
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.14.0
|
||||
NODE_VERSION: 18.7.0
|
||||
TERM: xterm
|
||||
|
||||
jobs:
|
||||
|
|
@ -19,11 +12,6 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- 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: machine-${{ matrix.runId }}-api-${{ 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 }}
|
||||
224
.github/workflows/e2e-detox-pr.yml
vendored
224
.github/workflows/e2e-detox-pr.yml
vendored
|
|
@ -1,224 +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: E2E
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
types:
|
||||
- labeled
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.event.pull_request.number }}-${{ github.event.label.name }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
INTUNE_ENABLED: '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-26
|
||||
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
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
||||
|
||||
- name: Set .env with RUNNING_E2E=true
|
||||
run: |
|
||||
echo "RUNNING_E2E=true" > .env
|
||||
|
||||
- name: Build iOS Simulator
|
||||
env:
|
||||
TAG: "${{ github.event.pull_request.head.sha }}"
|
||||
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
|
||||
run: bundle exec fastlane ios simulator --env ios.simulator skip_upload_to_s3_bucket:true
|
||||
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-*.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
|
||||
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
|
||||
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,
|
||||
});
|
||||
}
|
||||
});
|
||||
170
.github/workflows/e2e-detox-release.yml
vendored
170
.github/workflows/e2e-detox-release.yml
vendored
|
|
@ -1,170 +0,0 @@
|
|||
name: Detox E2E Tests Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release-*
|
||||
|
||||
env:
|
||||
INTUNE_ENABLED: 'true'
|
||||
|
||||
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-15
|
||||
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
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
||||
|
||||
- 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 }}
|
||||
169
.github/workflows/e2e-detox-scheduled.yml
vendored
169
.github/workflows/e2e-detox-scheduled.yml
vendored
|
|
@ -1,169 +0,0 @@
|
|||
name: Detox E2E Tests (Scheduled)
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * 4,5" # Wednesday and Thursday midnight
|
||||
|
||||
env:
|
||||
INTUNE_ENABLED: 'true'
|
||||
|
||||
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-15
|
||||
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
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
||||
|
||||
- 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 }}
|
||||
494
.github/workflows/e2e-ios-template.yml
vendored
494
.github/workflows/e2e-ios-template.yml
vendored
|
|
@ -1,494 +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 17 Pro"
|
||||
ios_device_os_name:
|
||||
description: "iPhone simulator OS version"
|
||||
required: false
|
||||
type: string
|
||||
default: "iOS 26.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: machine-${{ matrix.runId }}-os-${{ matrix.deviceOsVersion }}
|
||||
runs-on: macos-15
|
||||
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: Pre-boot iOS Simulator
|
||||
run: |
|
||||
set -e
|
||||
|
||||
echo "Looking for simulator: ${{ env.DEVICE_NAME }} with ${{ env.DEVICE_OS_VERSION }}"
|
||||
|
||||
# Find or create the simulator
|
||||
SIMULATOR_ID=$(xcrun simctl list devices | grep "${{ env.DEVICE_NAME }}" | grep "${{ env.DEVICE_OS_VERSION }}" | head -1 | grep -oE '([0-9A-F-]{36})' || true)
|
||||
|
||||
if [ -z "$SIMULATOR_ID" ]; then
|
||||
echo "Simulator not found. Creating simulator ${{ env.DEVICE_NAME }} with ${{ env.DEVICE_OS_VERSION }}"
|
||||
|
||||
# Get device type (use exact match with parentheses to avoid partial matches)
|
||||
DEVICE_TYPE=$(xcrun simctl list devicetypes | grep "${{ env.DEVICE_NAME }} (" | head -1 | awk -F'[()]' '{print $(NF-1)}')
|
||||
if [ -z "$DEVICE_TYPE" ]; then
|
||||
echo "Error: Could not find device type for ${{ env.DEVICE_NAME }}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found device type: $DEVICE_TYPE"
|
||||
|
||||
# Get runtime (extract the identifier after the last " - ")
|
||||
RUNTIME=$(xcrun simctl list runtimes | grep "${{ env.DEVICE_OS_VERSION }}" | head -1 | sed 's/.* - \(.*\)/\1/')
|
||||
if [ -z "$RUNTIME" ]; then
|
||||
echo "Error: Could not find runtime for ${{ env.DEVICE_OS_VERSION }}"
|
||||
exit 1
|
||||
fi
|
||||
echo "Found runtime: $RUNTIME"
|
||||
|
||||
# Create simulator
|
||||
SIMULATOR_ID=$(xcrun simctl create "CI-${{ env.DEVICE_NAME }}" "$DEVICE_TYPE" "$RUNTIME")
|
||||
echo "Created simulator with ID: $SIMULATOR_ID"
|
||||
else
|
||||
echo "Found existing simulator: $SIMULATOR_ID"
|
||||
fi
|
||||
|
||||
# Boot simulator first to initialize directory structure
|
||||
echo "Initializing simulator directories (first boot)..."
|
||||
xcrun simctl boot "$SIMULATOR_ID"
|
||||
xcrun simctl bootstatus "$SIMULATOR_ID"
|
||||
|
||||
# Shut down to apply password autofill restrictions
|
||||
echo "Shutting down to apply password autofill restrictions..."
|
||||
xcrun simctl shutdown "$SIMULATOR_ID"
|
||||
sleep 3
|
||||
|
||||
echo "Disabling password autofill (simulator is shut down)"
|
||||
|
||||
# Ensure the directory exists (should now exist after first boot)
|
||||
SETTINGS_DIR="$HOME/Library/Developer/CoreSimulator/Devices/$SIMULATOR_ID/data/Containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles"
|
||||
mkdir -p "$SETTINGS_DIR"
|
||||
|
||||
# Use the JavaScript utility to disable password autofill
|
||||
cd detox
|
||||
node utils/disable_ios_autofill.js --simulator-id "$SIMULATOR_ID"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "⚠️ Failed to disable password autofill"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ..
|
||||
|
||||
echo "Booting simulator with password autofill disabled..."
|
||||
xcrun simctl boot "$SIMULATOR_ID"
|
||||
|
||||
# Wait for boot to complete
|
||||
xcrun simctl bootstatus "$SIMULATOR_ID"
|
||||
|
||||
# Open Simulator.app in background to speed up UI rendering
|
||||
open -a Simulator --args -CurrentDeviceUDID "$SIMULATOR_ID"
|
||||
|
||||
# Give Simulator.app time to fully initialize UI (iOS 26.2 needs more time)
|
||||
sleep 5
|
||||
|
||||
echo "SIMULATOR_ID=$SIMULATOR_ID" >> $GITHUB_ENV
|
||||
|
||||
- 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: |
|
||||
echo "RUNNING_E2E=true" > .env
|
||||
|
||||
- name: Verify Simulator Health
|
||||
run: |
|
||||
echo "Verifying simulator health before running tests..."
|
||||
|
||||
# Check if simulator is still responsive
|
||||
if ! xcrun simctl bootstatus "$SIMULATOR_ID" -b 2>/dev/null; then
|
||||
echo "⚠️ Simulator not responsive, attempting recovery..."
|
||||
|
||||
# Try to shutdown and reboot
|
||||
xcrun simctl shutdown "$SIMULATOR_ID" || true
|
||||
sleep 3
|
||||
xcrun simctl boot "$SIMULATOR_ID"
|
||||
xcrun simctl bootstatus "$SIMULATOR_ID"
|
||||
fi
|
||||
|
||||
echo "✅ Simulator is healthy and ready"
|
||||
|
||||
- name: Start Metro Bundler and Run Detox E2E Tests
|
||||
continue-on-error: true # We want to run all the tests
|
||||
run: |
|
||||
# Start Metro bundler in background
|
||||
npm run start > metro.log 2>&1 &
|
||||
METRO_PID=$!
|
||||
echo "Metro PID: $METRO_PID"
|
||||
echo "METRO_PID=$METRO_PID" >> $GITHUB_ENV
|
||||
|
||||
# Wait for Metro to be ready with health check endpoint polling
|
||||
timeout=120
|
||||
elapsed=0
|
||||
echo "Waiting for Metro bundler to be ready..."
|
||||
while [ $elapsed -lt $timeout ]; do
|
||||
# Try health check endpoint first (more reliable)
|
||||
if curl -s http://localhost:8081/status 2>/dev/null | grep -q "packager-status:running"; then
|
||||
echo "✅ Metro bundler is ready after ${elapsed}s (verified via health check)"
|
||||
break
|
||||
# Fallback to log checking
|
||||
elif grep -q "Fast - Scalable - Integrated" metro.log 2>/dev/null; then
|
||||
echo "✅ Metro bundler is ready after ${elapsed}s (verified via logs)"
|
||||
break
|
||||
fi
|
||||
|
||||
# Show progress every 10 seconds
|
||||
if [ $((elapsed % 10)) -eq 0 ] && [ $elapsed -gt 0 ]; then
|
||||
echo "Still waiting for Metro... (${elapsed}s elapsed)"
|
||||
fi
|
||||
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
done
|
||||
|
||||
if [ $elapsed -ge $timeout ]; then
|
||||
echo "⚠️ Warning: Metro bundler may not be fully ready after ${timeout}s, proceeding anyway"
|
||||
echo "Metro process status:"
|
||||
ps aux | grep -E "metro|node.*start" | grep -v grep || echo "Metro process not found"
|
||||
echo ""
|
||||
echo "Last 30 lines of metro.log:"
|
||||
tail -30 metro.log
|
||||
else
|
||||
echo "Metro is serving on http://localhost:8081"
|
||||
# Give Metro extra time to fully initialize for iOS 26.2 (React Native bridge needs more time)
|
||||
echo "Allowing Metro to fully stabilize..."
|
||||
sleep 8
|
||||
fi
|
||||
|
||||
cd 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: Cleanup Simulator State
|
||||
if: always()
|
||||
run: |
|
||||
echo "Cleaning up simulator state after tests..."
|
||||
|
||||
# Stop Metro bundler if still running
|
||||
if [ ! -z "$METRO_PID" ]; then
|
||||
echo "Stopping Metro bundler (PID: $METRO_PID)..."
|
||||
kill -9 "$METRO_PID" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Terminate app gracefully
|
||||
if [ ! -z "$SIMULATOR_ID" ]; then
|
||||
echo "Terminating app on simulator..."
|
||||
xcrun simctl terminate "$SIMULATOR_ID" com.tokilabs.mattermost 2>/dev/null || true
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
echo "✅ Simulator cleanup complete"
|
||||
|
||||
- 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 }}
|
||||
178
.github/workflows/github-release.yml
vendored
178
.github/workflows/github-release.yml
vendored
|
|
@ -4,84 +4,56 @@ 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"
|
||||
INTUNE_ENABLED: 'true'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
||||
build-ios-unsigned:
|
||||
runs-on: macos-15-large
|
||||
runs-on: macos-12
|
||||
needs:
|
||||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/setup-ssh-key
|
||||
uses: ./.github/actions/setup-ssh-key
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
||||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
||||
|
||||
- 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 }}"
|
||||
TAG: "${{ github.ref_name }}"
|
||||
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
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.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
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
||||
- name: ci/prepare-android-build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
|
|
@ -90,145 +62,39 @@ jobs:
|
|||
|
||||
- name: ci/build-android-beta
|
||||
env:
|
||||
TAG: "${{ env.RELEASE_TAG }}"
|
||||
TAG: "${{ github.ref_name }}"
|
||||
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
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.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: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
|
||||
- uses: ruby/setup-ruby@cb0fda56a307b8c78d38320cd40d9eb22a3bf04e # v1.242.0
|
||||
- uses: ruby/setup-ruby@9669f3ee51dc3f4eda8447ab696b3ab19a90d14b # v1.144.0
|
||||
with:
|
||||
ruby-version: "2.7"
|
||||
|
||||
- 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
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
|
||||
- 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"
|
||||
|
|
|
|||
145
.github/workflows/validate-intune-clean.yml
vendored
145
.github/workflows/validate-intune-clean.yml
vendored
|
|
@ -1,145 +0,0 @@
|
|||
---
|
||||
name: Validate Intune Clean
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'ios/Podfile'
|
||||
- 'ios/Podfile.lock'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'ios/Mattermost/Info.plist'
|
||||
- 'ios/Mattermost/Mattermost.entitlements'
|
||||
- 'ios/Mattermost.xcodeproj/project.pbxproj'
|
||||
|
||||
jobs:
|
||||
validate-podfile-lock:
|
||||
name: Validate Podfile.lock is OSS-only
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Check Podfile.lock for Intune references
|
||||
run: |
|
||||
if grep -q "mattermost-intune\|IntuneMAMSwift" ios/Podfile.lock; then
|
||||
echo ""
|
||||
echo "❌ ERROR: Podfile.lock contains Intune dependencies"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "The committed Podfile.lock should only contain OSS dependencies."
|
||||
echo "Please restore the OSS version:"
|
||||
echo ""
|
||||
echo " git checkout main -- ios/Podfile.lock"
|
||||
echo ""
|
||||
echo "Or run: npm run intune:disable"
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Podfile.lock is clean (OSS-only)"
|
||||
|
||||
- name: Check for package.json Intune reference
|
||||
run: |
|
||||
# Check only dependencies and devDependencies sections (not scripts)
|
||||
if jq -e '.dependencies."@mattermost/intune" // .devDependencies."@mattermost/intune"' package.json > /dev/null 2>&1; then
|
||||
echo ""
|
||||
echo "❌ ERROR: package.json contains @mattermost/intune dependency"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "The Intune library should NOT be in package.json dependencies."
|
||||
echo "It is installed via --no-save flag during internal builds only."
|
||||
echo ""
|
||||
echo "The 'intune:link' script is OK - only dependencies are checked."
|
||||
echo ""
|
||||
echo "Please remove the @mattermost/intune entry from dependencies."
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ package.json is clean (no Intune in dependencies)"
|
||||
|
||||
- name: Check for package-lock.json Intune reference
|
||||
run: |
|
||||
if grep -q "@mattermost/intune" package-lock.json 2>/dev/null; then
|
||||
echo ""
|
||||
echo "❌ ERROR: package-lock.json contains @mattermost/intune"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "The Intune library should NOT be in package-lock.json."
|
||||
echo ""
|
||||
echo "Please run: npm install (without INTUNE_ENABLED set)"
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ package-lock.json is clean (no Intune reference)"
|
||||
|
||||
- name: Check Info.plist for Intune configuration
|
||||
run: |
|
||||
if grep -q "IntuneMAMSettings\|msauth\.com\.microsoft\.intunemam\|mattermost-intunemam\|mmauthbeta-intunemam\|intunemam-mtd\|msauthv2\|msauthv3" ios/Mattermost/Info.plist; then
|
||||
echo ""
|
||||
echo "❌ ERROR: Info.plist contains Intune configuration"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "The Info.plist should NOT contain Intune-specific settings."
|
||||
echo "Intune configuration is applied by Fastlane during internal builds only."
|
||||
echo ""
|
||||
echo "Please restore the OSS version:"
|
||||
echo ""
|
||||
echo " git checkout main -- ios/Mattermost/Info.plist"
|
||||
echo ""
|
||||
echo "Or run: npm run intune:disable"
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Info.plist is clean (no Intune configuration)"
|
||||
|
||||
- name: Check entitlements for Intune keychain groups
|
||||
run: |
|
||||
if grep -q "com\.microsoft\.adalcache\|com\.microsoft\.intune\.mam\|\.intunemam" ios/Mattermost/Mattermost.entitlements 2>/dev/null; then
|
||||
echo ""
|
||||
echo "❌ ERROR: Mattermost.entitlements contains Intune keychain groups"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "The entitlements should NOT contain Intune-specific keychain groups."
|
||||
echo "Intune keychain groups are added by Fastlane during internal builds only."
|
||||
echo ""
|
||||
echo "Please restore the OSS version:"
|
||||
echo ""
|
||||
echo " git checkout main -- ios/Mattermost/Mattermost.entitlements"
|
||||
echo ""
|
||||
echo "Or run: npm run intune:disable"
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Mattermost.entitlements is clean (no Intune keychain groups)"
|
||||
|
||||
- name: Check project.pbxproj for Intune frameworks
|
||||
run: |
|
||||
if grep -q "MSAL\|IntuneMAM\|IntuneMAMSwift" ios/Mattermost.xcodeproj/project.pbxproj 2>/dev/null; then
|
||||
echo ""
|
||||
echo "❌ ERROR: project.pbxproj contains Intune framework references"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "The project.pbxproj should NOT contain Intune framework references."
|
||||
echo "Intune frameworks (MSAL, IntuneMAM, IntuneMAMSwift) are added by Fastlane during internal builds only."
|
||||
echo ""
|
||||
echo "Please restore the OSS version:"
|
||||
echo ""
|
||||
echo " git checkout main -- ios/Mattermost.xcodeproj/project.pbxproj"
|
||||
echo ""
|
||||
echo "Or run: npm run intune:disable"
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ project.pbxproj is clean (no Intune framework references)"
|
||||
294
.gitignore
vendored
294
.gitignore
vendored
|
|
@ -1,199 +1,117 @@
|
|||
# Sorted combined gitignore from .gitignore and .gitignore_my
|
||||
|
||||
!build/notice-file
|
||||
!debug.keystore
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
!gradle-wrapper.jar
|
||||
!gradle-wrapper.properties
|
||||
!libraries/@mattermost/intune/.gitkeep
|
||||
!src/**/build/
|
||||
**/build/
|
||||
**/fastlane/.env
|
||||
**/fastlane/Preview.html
|
||||
**/fastlane/report.xml
|
||||
**/fastlane/screenshots
|
||||
**/fastlane/test_output
|
||||
*.aab
|
||||
*.apk
|
||||
*.hmap
|
||||
*.hprof
|
||||
*.iml
|
||||
*.ipa
|
||||
*.jks
|
||||
*.jsbundle
|
||||
*.keystore
|
||||
*.lcov
|
||||
*.log
|
||||
*.mattermost-license
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
*.pid
|
||||
*.pid.lock
|
||||
*.seed
|
||||
*.tgz
|
||||
*.tsbuildinfo
|
||||
*.un~
|
||||
*.xccheckout
|
||||
*.xcscmblueprint
|
||||
*.xcuserstate
|
||||
assets/override
|
||||
dist
|
||||
build-ios
|
||||
*.zip
|
||||
*/**/compass-icons.ttf
|
||||
*~
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
.AppleDouble
|
||||
.DS_Store
|
||||
.DocumentRevisions-V100
|
||||
.LSOverride
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trash-*
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
._*
|
||||
.aider*
|
||||
.apdisk
|
||||
.buckconfig.local
|
||||
.buckd/
|
||||
.buckversion
|
||||
.bundle
|
||||
.cache
|
||||
.cache/
|
||||
.classpath
|
||||
.claude/settings.local.json
|
||||
.com.apple.timemachine.donotpresent
|
||||
.cxx/
|
||||
.directory
|
||||
.docusaurus
|
||||
.dynamodb/
|
||||
server.PID
|
||||
mattermost.keystore
|
||||
tmp/
|
||||
.env
|
||||
.env.development.local
|
||||
.env.local
|
||||
.env.production.local
|
||||
.env.test.local
|
||||
.eslintcache
|
||||
.expo
|
||||
.externalNativeBuild/
|
||||
.fakebuckversion
|
||||
.fseventsd
|
||||
.fuse_hidden*
|
||||
.fusebox/
|
||||
.gradle
|
||||
.gradle/
|
||||
.gradletasknamecache
|
||||
.grunt
|
||||
.idea
|
||||
.idea/
|
||||
.lock-wscript
|
||||
.metro-health-check*
|
||||
.netrwhist
|
||||
.next
|
||||
.nfs*
|
||||
.node_repl_history
|
||||
.npm
|
||||
.npminstall
|
||||
.nuxt
|
||||
.nyc_output
|
||||
.parcel-cache
|
||||
.pnp.*
|
||||
.pnpm-debug.log*
|
||||
.podinstall
|
||||
.project
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
.scannerwork
|
||||
.serverless/
|
||||
.settings
|
||||
.stylelintcache
|
||||
.temp
|
||||
.tern-port
|
||||
.tmp
|
||||
.tsbuildinfo.precommit
|
||||
.vscode/*
|
||||
!.vscode/launch.json
|
||||
!.vscode/settings.json
|
||||
.vscode-test
|
||||
.vuepress/dist
|
||||
.yarn-integrity
|
||||
.yarn/build-state.yml
|
||||
.yarn/cache
|
||||
.yarn/install-state.gz
|
||||
.yarn/unplugged
|
||||
.yarninstall
|
||||
env.d.ts
|
||||
*.apk
|
||||
*.aab
|
||||
*.ipa
|
||||
|
||||
*/**/compass-icons.ttf
|
||||
|
||||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
||||
# Xcode
|
||||
#
|
||||
ios/build/*
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
Icon
|
||||
Network Trash Folder
|
||||
Session.vim
|
||||
Temporary Items
|
||||
[._]*.s[a-w][a-z]
|
||||
[._]s[a-w][a-z]
|
||||
__generated__
|
||||
*.hmap
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
ios/Pods
|
||||
.podinstall
|
||||
ios/.xcode.env.local
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
*.hprof
|
||||
.cxx/
|
||||
*.keystore
|
||||
!debug.keystore
|
||||
android/app/bin
|
||||
android/app/build
|
||||
android/app/src/main/res/raw/*
|
||||
android/build
|
||||
.settings
|
||||
.project
|
||||
.classpath
|
||||
|
||||
# node.js
|
||||
#
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
.npminstall
|
||||
yarn-error.log
|
||||
.yarninstall
|
||||
|
||||
# Vim
|
||||
[._]*.s[a-w][a-z]
|
||||
[._]s[a-w][a-z]
|
||||
*.un~
|
||||
Session.vim
|
||||
.netrwhist
|
||||
*~
|
||||
tags
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/
|
||||
|
||||
**/fastlane/report.xml
|
||||
**/fastlane/Preview.html
|
||||
**/fastlane/screenshots
|
||||
**/fastlane/test_output
|
||||
**/fastlane/.env
|
||||
|
||||
# Sentry
|
||||
android/sentry.properties
|
||||
assets/override
|
||||
bower_components
|
||||
buck-out/
|
||||
build-ios
|
||||
build/Release
|
||||
captures/
|
||||
ios/sentry.properties
|
||||
|
||||
# Testing
|
||||
.nyc_output
|
||||
coverage
|
||||
deploymentTargetDropDown.xml
|
||||
.tmp
|
||||
|
||||
# E2E testing
|
||||
mattermost-license.txt
|
||||
*.mattermost-license
|
||||
detox/artifacts
|
||||
detox/detox_pixel_*
|
||||
dist
|
||||
env.d.ts
|
||||
google-services.json
|
||||
gradle-app.setting
|
||||
ios/.xcode.env.local
|
||||
ios/Pods
|
||||
ios/build/*
|
||||
ios/sentry.properties
|
||||
jspm_packages/
|
||||
lerna-debug.log*
|
||||
lib-cov
|
||||
libraries/**/**/.build
|
||||
libraries/**/**/build
|
||||
libraries/@mattermost/intune/*
|
||||
local.properties
|
||||
logs
|
||||
mattermost-license.txt
|
||||
mattermost.keystore
|
||||
misc.xml
|
||||
node_modules/
|
||||
node_modules/@mattermost/intune
|
||||
npm-debug.log
|
||||
npm-debug.log*
|
||||
out
|
||||
output.json
|
||||
pids
|
||||
project.xcworkspace
|
||||
render.experimental.xml
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
server.PID
|
||||
tags
|
||||
tmp/
|
||||
web_modules/
|
||||
xcuserdata
|
||||
xcuserdata/
|
||||
yarn-debug.log*
|
||||
yarn-error.log
|
||||
yarn-error.log*
|
||||
|
||||
# 새로 적용된 .gitignore 규칙에 의해 무시되었으나, 기존 프로젝트에서 관리(Tracking)되고 있던 필수 파일들 예외 처리
|
||||
!android/app/debug.keystore
|
||||
!android/app/google-services.json
|
||||
!build/notice-file/Readme.md
|
||||
!build/notice-file/config.yaml
|
||||
!env.d.ts
|
||||
!fastlane/screenshots/README.txt
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
.bundle
|
||||
|
||||
#editor-settings
|
||||
.vscode
|
||||
.scannerwork
|
||||
launch.json
|
||||
|
||||
# Notice.txt generation
|
||||
!build/notice-file
|
||||
|
||||
# Temporary files created by Metro to check the health of the file watcher
|
||||
.metro-health-check*
|
||||
|
|
|
|||
11
.gitmodules
vendored
11
.gitmodules
vendored
|
|
@ -1,11 +0,0 @@
|
|||
# Intune MAM library submodule (private repository - internal builds only)
|
||||
# To pin to a specific tag/commit:
|
||||
# cd libraries/@mattermost/intune
|
||||
# git checkout <tag-or-commit>
|
||||
# cd ../..
|
||||
# git add libraries/@mattermost/intune
|
||||
# git commit -m "Update Intune submodule to <version>"
|
||||
|
||||
[submodule "libraries/@mattermost/intune"]
|
||||
path = libraries/@mattermost/intune
|
||||
url = git@github.com:mattermost/mattermost-mobile-intune.git
|
||||
|
|
@ -1 +1,4 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
sh ./scripts/pre-commit.sh
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# 필수 준수 사항
|
||||
- agent-ops/rules/global.md 파일을 읽고 이에 대해 꼭 준수해서 진행해야한다.
|
||||
- global.md 내용을 모르면 추정하지 말고 먼저 해당 파일을 확인한다.
|
||||
|
|
@ -1 +0,0 @@
|
|||
-
|
||||
|
|
@ -1 +0,0 @@
|
|||
-
|
||||
|
|
@ -1 +0,0 @@
|
|||
-
|
||||
|
|
@ -1 +0,0 @@
|
|||
-
|
||||
|
|
@ -1 +0,0 @@
|
|||
-
|
||||
|
|
@ -1 +0,0 @@
|
|||
-
|
||||
|
|
@ -1 +1 @@
|
|||
22.14.0
|
||||
18.17
|
||||
|
|
|
|||
2
.nvmrc
2
.nvmrc
|
|
@ -1 +1 @@
|
|||
22.14.0
|
||||
18.17
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.2.0
|
||||
2.7.8
|
||||
|
|
|
|||
|
|
@ -32,14 +32,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.12.1",
|
||||
"platform": "darwin"
|
||||
}
|
||||
],
|
||||
|
|
|
|||
31
.vscode/launch.json
vendored
31
.vscode/launch.json
vendored
|
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Metro: Start",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "bash",
|
||||
"runtimeArgs": [
|
||||
"-c",
|
||||
"lsof -ti:8081 | xargs kill -9 2>/dev/null; npm start"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
},
|
||||
{
|
||||
"name": "Android: Run App",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "bash",
|
||||
"runtimeArgs": [
|
||||
"-c",
|
||||
"adb devices | grep -q emulator || (emulator -avd Pixel_7_Pro &); adb -e wait-for-device && npm run android -- --deviceId emulator-5554"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
.vscode/settings.json
vendored
17
.vscode/settings.json
vendored
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"react-native.packager.port": 8081,
|
||||
"terminal.integrated.env.osx": {
|
||||
"NVM_DIR": "${env:HOME}/.nvm",
|
||||
"JAVA_HOME": "/opt/homebrew/opt/openjdk@17",
|
||||
"ANDROID_HOME": "${env:HOME}/Library/Android/sdk",
|
||||
"ANDROID_SDK_ROOT": "${env:HOME}/Library/Android/sdk"
|
||||
},
|
||||
"terminal.integrated.profiles.osx": {
|
||||
"zsh": {
|
||||
"path": "/bin/zsh",
|
||||
"args": ["-l"]
|
||||
}
|
||||
},
|
||||
"terminal.integrated.defaultProfile.osx": "zsh",
|
||||
"npm.scriptExplorerAction": "run"
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# 필수 준수 사항
|
||||
- agent-ops/rules/global.md 파일을 읽고 이에 대해 꼭 준수해서 진행해야한다.
|
||||
- global.md 내용을 모르면 추정하지 말고 먼저 해당 파일을 확인한다.
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
---
|
||||
name: scaffold-agent-ops
|
||||
description: 프로젝트 상태를 판별하고 에이전트 진입 파일과 agent-ops 기본 스캐폴드를 생성하기 위한 초기 규칙
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# 목적
|
||||
|
||||
이 스킬은 프로젝트에 Agent-Ops 구조가 없거나 불완전할 때,
|
||||
현재 프로젝트 상태를 분석하여 다음을 세팅한다.
|
||||
|
||||
1. 에이전트 진입 파일
|
||||
2. agent-ops 기본 폴더 구조
|
||||
3. global rule
|
||||
4. domain rule 템플릿
|
||||
5. skill router
|
||||
6. skill 템플릿
|
||||
7. 초기 domain / skill 세팅
|
||||
|
||||
이 스킬은 과도한 초기 생성보다
|
||||
"현재 프로젝트에 맞는 최소 스캐폴드" 생성을 우선한다.
|
||||
|
||||
# 핵심 원칙
|
||||
|
||||
- 기존 구조를 우선한다.
|
||||
- 새 파일 생성은 꼭 필요한 최소 범위로 제한한다.
|
||||
- 에이전트별 md는 얇은 진입점으로 둔다.
|
||||
- 실제 규칙은 agent-ops 아래로 모은다.
|
||||
- 도메인은 발명하지 말고 현재 구조에서 발견한다.
|
||||
- 처음에는 핵심 도메인만 생성한다.
|
||||
- 반복되는 작업만 초기 skill로 만든다.
|
||||
- 불확실한 내용은 후보로 제시한다.
|
||||
|
||||
# 상태 판별
|
||||
|
||||
## 신규 프로젝트
|
||||
다음 조건이 많으면 신규 프로젝트로 본다.
|
||||
|
||||
- 코드/폴더 구조가 단순하다
|
||||
- 도메인 경계가 아직 약하다
|
||||
- Agent-Ops 관련 파일이 없다
|
||||
- 반복 작업 패턴이 아직 적다
|
||||
|
||||
## 운영중 프로젝트
|
||||
다음 조건이 많으면 운영중 프로젝트로 본다.
|
||||
|
||||
- 모듈/폴더/패키지 경계가 보인다
|
||||
- 반복 작업이 드러난다
|
||||
- 기존 규칙/문서/관례의 흔적이 있다
|
||||
- 핵심 책임 경계가 식별된다
|
||||
|
||||
# 생성 대상
|
||||
|
||||
이 스킬은 아래 구조를 기준으로 scaffold를 제안하거나 생성한다.
|
||||
|
||||
- CLAUDE.md
|
||||
- GEMINI.md
|
||||
- .cursorrules
|
||||
- agent-ops/rules/global.md
|
||||
- agent-ops/rules/_templates/domain-rule-template.md
|
||||
- agent-ops/rules/domain/<domain>/rules.md
|
||||
- agent-ops/skills/router.md
|
||||
- agent-ops/skills/_templates/skill-template.md
|
||||
|
||||
필요 시 아래 초기 skill도 함께 제안한다.
|
||||
|
||||
- agent-ops/skills/find-domain/SKILL.md
|
||||
- agent-ops/skills/code-change/SKILL.md
|
||||
|
||||
# 에이전트 진입 파일 원칙
|
||||
|
||||
CLAUDE.md, GEMINI.md, .cursorrules 는 공통적으로 얇게 유지한다.
|
||||
|
||||
각 파일에는 아래만 둔다.
|
||||
|
||||
- 응답 언어
|
||||
- 기존 구조 우선
|
||||
- 새 파일 생성보다 기존 수정 우선
|
||||
- 작업 전 agent-ops/skills/router.md 참조
|
||||
- 관련 rule / skill 만 최소 로드
|
||||
- 세부 규칙은 agent-ops 아래 파일을 따른다
|
||||
|
||||
각 에이전트 진입 파일에 프로젝트별 상세 규칙을 중복 작성하지 않는다.
|
||||
|
||||
# Rule 구조 원칙
|
||||
|
||||
## global.md
|
||||
전 프로젝트 공통 규칙만 둔다.
|
||||
|
||||
포함 항목:
|
||||
- 응답 언어
|
||||
- 기존 구조 우선
|
||||
- 새 파일 생성보다 기존 수정 우선
|
||||
- 코드 변경 전 관련 domain rule 확인
|
||||
- 요청 범위를 넘는 변경 금지
|
||||
- 불확실하면 단정하지 말고 후보 제시
|
||||
- 플랫폼 특화 규칙은 global에 두지 않음
|
||||
|
||||
## domain rule
|
||||
각 도메인 rules.md 는 아래만 둔다.
|
||||
|
||||
- 목적 / 책임
|
||||
- 포함 경로
|
||||
- 제외 경로
|
||||
- 주요 구성 요소
|
||||
- 유지할 패턴
|
||||
- 다른 도메인과의 경계
|
||||
- 금지 사항
|
||||
|
||||
# DDD 기준
|
||||
|
||||
도메인은 아래 기준으로 분류한다.
|
||||
|
||||
## Core Domain
|
||||
핵심 가치와 주요 유즈케이스를 담당하는 영역
|
||||
|
||||
## Supporting Domain
|
||||
핵심 도메인을 지원하는 영역
|
||||
|
||||
## Generic / Common
|
||||
여러 도메인이 공통으로 사용하는 범용 영역
|
||||
|
||||
도메인은 반드시 실제 폴더, 모듈, 패키지, 네임스페이스, 책임 경계와 연결되어야 한다.
|
||||
|
||||
# Skill 구조 원칙
|
||||
|
||||
## router.md
|
||||
router.md 는 작업 유형에 따라 필요한 skill 또는 domain rule만 로드하게 한다.
|
||||
|
||||
최소 라우팅 축:
|
||||
- 구조 분석 / 스캐폴드
|
||||
- 도메인 판별
|
||||
- 코드 변경
|
||||
- 흐름 추적
|
||||
- 문서/릴리즈 정리
|
||||
|
||||
처음부터 너무 많은 분기를 만들지 않는다.
|
||||
|
||||
## skill-template.md
|
||||
각 skill 은 아래 형식을 따른다.
|
||||
|
||||
- 목적
|
||||
- 언제 호출할지
|
||||
- 입력
|
||||
- 먼저 확인할 것
|
||||
- 실행 절차
|
||||
- 출력 형식
|
||||
- 금지 사항
|
||||
|
||||
# 신규 프로젝트 처리
|
||||
|
||||
신규 프로젝트일 때는 템플릿 중심으로 세팅한다.
|
||||
|
||||
1. 언어/구조를 확인한다.
|
||||
2. 에이전트 진입 파일 3개를 얇게 생성한다.
|
||||
3. agent-ops/rules/global.md 생성
|
||||
4. domain-rule-template.md 생성
|
||||
5. skills/router.md 생성
|
||||
6. skill-template.md 생성
|
||||
7. 핵심 domain placeholder 2~4개만 제안
|
||||
8. 초기 skill 은 최소 2개만 제안한다.
|
||||
|
||||
신규 프로젝트에서는 domain rules 를 과도하게 채우지 않는다.
|
||||
|
||||
# 운영중 프로젝트 처리
|
||||
|
||||
운영중 프로젝트일 때는 현재 구조 기반으로 자동 채움한다.
|
||||
|
||||
1. 구조를 분석한다.
|
||||
2. Core / Supporting / Generic 도메인 후보를 식별한다.
|
||||
3. 실제 경로를 반영한 domain rules 초안을 제안한다.
|
||||
4. 반복 작업 기반으로 필요한 초기 skill 을 제안한다.
|
||||
5. router.md 에 기본 라우팅을 채운다.
|
||||
6. 신규 프로젝트보다 한 단계 더 구체적으로 scaffold 한다.
|
||||
|
||||
운영중 프로젝트에서는 실제 코드 구조에 맞는 domain / skill 을 일부 채워 넣는다.
|
||||
|
||||
# 출력 형식
|
||||
|
||||
## 상태 판별
|
||||
- Agent-Ops 상태: 없음 / 부분 적용 / 운영중
|
||||
- 프로젝트 상태: 신규 / 운영중
|
||||
- 근거: 짧게 요약
|
||||
|
||||
## 스캐폴드 계획
|
||||
- 생성할 파일
|
||||
- 바로 채울 파일
|
||||
- placeholder 로 둘 파일
|
||||
|
||||
## 도메인 제안
|
||||
- Core Domain
|
||||
- Supporting Domain
|
||||
- Generic / Common
|
||||
|
||||
## 초기 skill 제안
|
||||
- skill 이름
|
||||
- 필요한 이유
|
||||
|
||||
## 주의사항
|
||||
- 지금 만들지 말아야 할 것
|
||||
- 아직 확정하면 안 되는 것
|
||||
|
||||
# 금지
|
||||
|
||||
- 플랫폼 전용 규칙을 기본 scaffold 에 넣지 않는다.
|
||||
- agent 별 md 에 상세 규칙을 중복 작성하지 않는다.
|
||||
- 실제 구조보다 앞선 추상 구조를 강요하지 않는다.
|
||||
- 처음부터 많은 domain / skill 을 만들지 않는다.
|
||||
- scaffold 단계에서 불필요한 세부 구현까지 생성하지 않는다.
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
# iOS Simulator Setup Guide for AI Agents
|
||||
|
||||
This guide documents the process for setting up and running the Mattermost Mobile app on an iOS simulator.
|
||||
|
||||
## Prerequisites Check
|
||||
|
||||
Before starting, verify these are installed:
|
||||
|
||||
```bash
|
||||
# Check Xcode
|
||||
xcodebuild -version
|
||||
|
||||
# Check CocoaPods
|
||||
pod --version
|
||||
|
||||
# Check Node.js
|
||||
node --version
|
||||
|
||||
# Check npm dependencies
|
||||
test -d node_modules && echo "node_modules exists" || echo "Run: npm install"
|
||||
```
|
||||
|
||||
## Step 1: iOS Simulator Runtime
|
||||
|
||||
Check if iOS simulators are available:
|
||||
|
||||
```bash
|
||||
xcrun simctl list devices available
|
||||
```
|
||||
|
||||
If no devices are listed or you see "Install Started", you need to download the iOS platform:
|
||||
|
||||
```bash
|
||||
xcodebuild -downloadPlatform iOS
|
||||
```
|
||||
|
||||
**Note:** This downloads ~8GB and takes several minutes. The command runs in the foreground and shows progress.
|
||||
|
||||
## Step 2: CocoaPods Installation
|
||||
|
||||
### Critical: Set UTF-8 Encoding
|
||||
|
||||
CocoaPods requires UTF-8 encoding. Always set this before running pod commands:
|
||||
|
||||
```bash
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
```
|
||||
|
||||
### Install Pods (Apple Silicon Mac)
|
||||
|
||||
```bash
|
||||
cd ios
|
||||
RCT_NEW_ARCH_ENABLED=0 arch -x86_64 pod install
|
||||
```
|
||||
|
||||
### Common CocoaPods Issues
|
||||
|
||||
#### Issue: "Unicode Normalization not appropriate for ASCII-8BIT"
|
||||
**Solution:** Set UTF-8 encoding as shown above.
|
||||
|
||||
#### Issue: Podfile.lock version conflicts
|
||||
**Solution:** Clean install:
|
||||
```bash
|
||||
cd ios
|
||||
rm -rf Pods Podfile.lock build
|
||||
export LANG=en_US.UTF-8 && export LC_ALL=en_US.UTF-8
|
||||
RCT_NEW_ARCH_ENABLED=0 arch -x86_64 pod install
|
||||
```
|
||||
|
||||
#### Issue: GitHub SSH timeouts during pod install
|
||||
**Symptom:** Errors like "Connection to github.com port 22: Operation timed out"
|
||||
|
||||
**Solution:** Temporarily configure git to use HTTPS instead of SSH:
|
||||
```bash
|
||||
git config --global url."https://github.com/".insteadOf git@github.com:
|
||||
```
|
||||
|
||||
**IMPORTANT:** Revert this after pod install completes, or the user won't be able to push:
|
||||
```bash
|
||||
git config --global --unset url.https://github.com/.insteadof
|
||||
```
|
||||
|
||||
**Warning:** Never leave git URL rewrites in place without informing the user!
|
||||
|
||||
## Step 3: Start Metro Bundler (if not already running)
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
Wait for "Dev server ready" message before proceeding.
|
||||
|
||||
## Step 4: Run iOS App (if not already built)
|
||||
|
||||
### Using npm script
|
||||
```bash
|
||||
npm run ios -- --simulator="iPhone 17 Pro"
|
||||
```
|
||||
|
||||
### Or specify a different simulator
|
||||
List available simulators first:
|
||||
```bash
|
||||
xcrun simctl list devices available | grep -E "iPhone|iPad"
|
||||
```
|
||||
|
||||
Then run with your chosen device:
|
||||
```bash
|
||||
npm run ios -- --simulator="iPhone 16e"
|
||||
```
|
||||
|
||||
## Build Times
|
||||
|
||||
- **First build:** 10-30 minutes (compiles all native code)
|
||||
- **Subsequent builds:** Much faster (incremental)
|
||||
- **JS/TS changes:** ~3 seconds hot reload (no native rebuild needed)
|
||||
|
||||
## Troubleshooting Build Failures
|
||||
|
||||
### Getting Better Error Messages
|
||||
|
||||
If `npm run ios` fails, run xcodebuild directly for clearer errors:
|
||||
|
||||
```bash
|
||||
xcodebuild -workspace ios/Mattermost.xcworkspace \
|
||||
-configuration Debug \
|
||||
-scheme Mattermost \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
|
||||
2>&1 | grep -E "error:|fatal error"
|
||||
```
|
||||
|
||||
### Clean Build
|
||||
|
||||
If builds fail mysteriously, try a clean build:
|
||||
|
||||
```bash
|
||||
# Clean Xcode derived data
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/Mattermost-*
|
||||
|
||||
# Clean and reinstall pods
|
||||
cd ios
|
||||
rm -rf Pods build
|
||||
export LANG=en_US.UTF-8 && export LC_ALL=en_US.UTF-8
|
||||
RCT_NEW_ARCH_ENABLED=0 arch -x86_64 pod install
|
||||
```
|
||||
|
||||
### Opening in Xcode
|
||||
|
||||
For complex build issues, open in Xcode for better debugging:
|
||||
|
||||
```bash
|
||||
open ios/Mattermost.xcworkspace
|
||||
```
|
||||
|
||||
Then select your simulator target and press Cmd+B to build.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
# Full setup sequence (Apple Silicon)
|
||||
export LANG=en_US.UTF-8 && export LC_ALL=en_US.UTF-8
|
||||
cd ios && RCT_NEW_ARCH_ENABLED=0 arch -x86_64 pod install && cd ..
|
||||
npm start &
|
||||
sleep 10
|
||||
npm run ios -- --simulator="iPhone 17 Pro"
|
||||
```
|
||||
|
||||
## Do NOT Modify
|
||||
|
||||
- **Git configuration:** Never permanently change git URL rewrites without reverting
|
||||
- **Podfile:** Don't add modular_headers to pods unless you understand the implications
|
||||
- **New Architecture:** Keep `RCT_NEW_ARCH_ENABLED=0` as the project uses the old architecture
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# 필수 준수 사항
|
||||
- agent-ops/rules/global.md 파일을 읽고 이에 대해 꼭 준수해서 진행해야한다.
|
||||
- global.md 내용을 모르면 추정하지 말고 먼저 해당 파일을 확인한다.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# 필수 준수 사항
|
||||
- agent-ops/rules/global.md 파일을 읽고 이에 대해 꼭 준수해서 진행해야한다.
|
||||
- global.md 내용을 모르면 추정하지 말고 먼저 해당 파일을 확인한다.
|
||||
4
Gemfile
4
Gemfile
|
|
@ -1,4 +0,0 @@
|
|||
source "https://rubygems.org"
|
||||
|
||||
gem "cocoapods", "1.16.1"
|
||||
gem "rexml", ">= 3.4.2"
|
||||
1437
NOTICE.txt
1437
NOTICE.txt
File diff suppressed because it is too large
Load diff
36
README.md
36
README.md
|
|
@ -1,7 +1,7 @@
|
|||
# Mattermost Mobile v2
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (10.11.0+)
|
||||
- **Supported iOS versions:** 16.0+
|
||||
- **Minimum Server versions:** Current ESR version (8.1.0+)
|
||||
- **Supported iOS versions:** 12.4+
|
||||
- **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).
|
||||
|
|
@ -12,34 +12,6 @@ We plan on releasing monthly updates with new features - check the [changelog](h
|
|||
|
||||
**Important:** If you self-compile the Mattermost Mobile apps you also need to deploy your own [Mattermost Push Notification Service](https://github.com/mattermost/mattermost-push-proxy/releases).
|
||||
|
||||
# Android 16KB Page Size Support
|
||||
|
||||
**Temporary Requirement:** To comply with Google Play's 16KB page size requirement for Android devices, this project includes a compatibility patch that must be applied before building for Android.
|
||||
|
||||
### When to Apply the Patch
|
||||
|
||||
- **CI/CD Builds:** The patch is automatically applied in GitHub Actions for Android builds
|
||||
- **Local Development:** If you're building Android locally and encounter 16KB page size related issues, run:
|
||||
|
||||
```bash
|
||||
npm run apply-16kb-pagesize-patch
|
||||
```
|
||||
|
||||
This script will:
|
||||
1. Update package dependencies to compatible versions
|
||||
2. Apply necessary code changes for 16KB page size support
|
||||
3. Update patch files for modified dependencies
|
||||
4. Regenerate `package-lock.json`
|
||||
|
||||
### ⚠️ Important Warnings
|
||||
|
||||
- **DO NOT commit the changes** applied by this patch to the repository
|
||||
- **These changes will break iOS builds** if committed
|
||||
- The patch is designed to be applied only during Android CI builds
|
||||
- For local development, revert all changes after building Android
|
||||
|
||||
**Note:** This is a temporary solution until all dependencies natively support 16KB page sizes.
|
||||
|
||||
# How to Contribute
|
||||
|
||||
### Testing
|
||||
|
|
@ -47,7 +19,7 @@ This script will:
|
|||
To help with testing app updates before they're released, you can:
|
||||
|
||||
1. Sign up to be a beta tester
|
||||
- [Android](https://play.google.com/apps/testing/com.tokilabs.mattermost)
|
||||
- [Android](https://play.google.com/apps/testing/com.mattermost.rnbeta)
|
||||
- [iOS](https://testflight.apple.com/join/Q7Rx7K9P) - Open this link from your iOS device
|
||||
2. Install the `Mattermost Beta` app. New updates in the Beta app are released periodically. You will receive a notification when the new updates are available.
|
||||
3. File any bugs you find by filing a [GitHub issue](https://github.com/mattermost/mattermost-mobile/issues) with:
|
||||
|
|
@ -59,7 +31,7 @@ To help with testing app updates before they're released, you can:
|
|||
- 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
|
||||
|
||||
You can leave the Beta testing program at any time:
|
||||
- On Android, [click this link](https://play.google.com/apps/testing/com.tokilabs.mattermost) while logged in with your Google Play email address used to opt-in for the Beta program, then click **Leave the program**.
|
||||
- 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**.
|
||||
- On iOS, access the `Mattermost Beta` app page in TestFlight and click **Stop Testing**.
|
||||
|
||||
### Contribute Code
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
---
|
||||
name: <domain-name>
|
||||
description: <도메인 한 줄 설명>
|
||||
type: core | supporting | generic
|
||||
---
|
||||
|
||||
# <Domain Name>
|
||||
|
||||
## 목적 / 책임
|
||||
<이 도메인이 담당하는 핵심 책임>
|
||||
|
||||
## 포함 경로
|
||||
- `app/<path>/`
|
||||
- `app/actions/local/<file>.ts`
|
||||
- `app/actions/remote/<file>.ts`
|
||||
|
||||
## 제외 경로
|
||||
- <이 도메인에 속하지 않는 경로>
|
||||
|
||||
## 주요 구성 요소
|
||||
- **<component>**: <역할>
|
||||
- **<component>**: <역할>
|
||||
|
||||
## 유지할 패턴
|
||||
- <이 도메인에서 따라야 할 패턴>
|
||||
|
||||
## 다른 도메인과의 경계
|
||||
- **database 도메인**: DB 조작은 database 도메인 패턴을 따른다
|
||||
- **network 도메인**: API 호출은 ClientMix 패턴을 따른다
|
||||
|
||||
## 금지 사항
|
||||
- <이 도메인에서 하면 안 되는 것>
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
---
|
||||
name: database
|
||||
description: WatermelonDB 이중 DB 시스템 - 앱 DB와 서버별 DB
|
||||
type: supporting
|
||||
---
|
||||
|
||||
# Database Domain
|
||||
|
||||
## 목적 / 책임
|
||||
WatermelonDB 기반 이중 데이터베이스를 관리한다. DatabaseManager 싱글톤, operator, 스키마, 모델을 담당한다.
|
||||
|
||||
## 포함 경로
|
||||
- `app/database/` (전체)
|
||||
- `app/database/manager/` - DatabaseManager 싱글톤
|
||||
- `app/database/models/app/` - 앱 전역 모델 (서버 목록 등)
|
||||
- `app/database/models/server/` - 서버별 모델 (채널, 유저, 포스트 등)
|
||||
- `app/database/operator/` - 데이터 쓰기 operator
|
||||
- `app/database/schema/` - DB 스키마 정의
|
||||
- `docs/database/` - DB 스키마 문서
|
||||
|
||||
## 제외 경로
|
||||
- `app/queries/` (Query Layer는 별도 - 여기서 DB를 읽는다)
|
||||
|
||||
## 주요 구성 요소
|
||||
- **DatabaseManager**: 모든 DB 인스턴스 관리 싱글톤 (`app/database/manager/index.ts`)
|
||||
- **App Database**: 서버 목록 등 전역 앱 상태 (하나만 존재)
|
||||
- **Server Database**: 서버별 채널/유저/포스트 데이터 (서버당 하나)
|
||||
- **Operators**: transformer/handler/comparator 패턴으로 데이터 쓰기
|
||||
- **Schema**: 테이블 정의 및 마이그레이션
|
||||
|
||||
## 유지할 패턴
|
||||
- DB 경로: iOS는 App Group, Android는 `${documentDirectory}/databases/`
|
||||
- Operator를 통해 batch 작업으로 데이터 쓰기
|
||||
- Sync handler는 create/update/delete 전체 라이프사이클 처리 필수
|
||||
- 스키마 변경 시 반드시 버전 번호 증가 + 문서(`docs/database/`) 업데이트
|
||||
- Query Layer(`app/queries/`)는 `query*`, `observe*`, `get*`, `prepare*` 패턴
|
||||
|
||||
## 다른 도메인과의 경계
|
||||
- **모든 도메인**: DB 직접 접근보다 Query Layer(`app/queries/`) 사용 권장
|
||||
- **network 도메인**: remote action이 API 응답을 operator에 전달
|
||||
|
||||
## 금지 사항
|
||||
- 스키마 변경 후 `docs/database/` 문서 미업데이트
|
||||
- Sync handler에서 stale 레코드 삭제 누락 (`prepareDestroyPermanently()` 필수)
|
||||
- 테스트에서 `extraLokiOptions: {autosave: false}` 누락 (메모리 누수)
|
||||
- 테스트 후 `DatabaseManager.destroyServerDatabase()` cleanup 누락
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
---
|
||||
name: messaging
|
||||
description: 채널, 포스트, 스레드, 드래프트 등 핵심 메시징 기능
|
||||
type: core
|
||||
---
|
||||
|
||||
# Messaging Domain
|
||||
|
||||
## 목적 / 책임
|
||||
Mattermost의 핵심 메시징 기능: 채널, 포스트, 스레드, 리액션, 드래프트, 예약 포스트를 담당한다.
|
||||
|
||||
## 포함 경로
|
||||
- `app/actions/local/channel.ts`, `post.ts`, `thread.ts`, `draft.ts`, `reactions.ts`, `scheduled_post.ts`
|
||||
- `app/actions/remote/channel.ts`, `post.ts`, `thread.ts`, `reactions.ts`, `search.ts`
|
||||
- `app/queries/servers/channel.ts`, `post.ts`, `thread.ts`, `drafts.ts`, `scheduled_post.ts`, `reaction.ts`
|
||||
- `app/database/models/server/channel*.ts`, `post.ts`, `thread*.ts`, `draft.ts`, `reaction.ts`, `scheduled_post.ts`
|
||||
- `app/screens/channel/`, `app/screens/thread/`, `app/screens/edit_post/`
|
||||
- `app/components/post_list/`, `app/components/post_draft/`
|
||||
|
||||
## 제외 경로
|
||||
- `app/products/` (products는 각 product 도메인)
|
||||
- `app/actions/remote/user.ts` (user 도메인)
|
||||
|
||||
## 주요 구성 요소
|
||||
- **Channel**: 채널 CRUD, 멤버십, 카테고리 관리
|
||||
- **Post**: 포스트 생성/수정/삭제, 파일 첨부, 리액션
|
||||
- **Thread**: 스레드 팔로우, 언급, 참여자 관리
|
||||
- **Draft**: 임시저장, 예약 포스트
|
||||
- **Search**: 채널/포스트 검색
|
||||
|
||||
## 유지할 패턴
|
||||
- Remote action: `fetchXxx()` → API 호출 → operator로 DB 저장 → `{error}` 반환
|
||||
- Local action: DB 조작만 수행
|
||||
- Sync handler는 create/update/delete 전체 라이프사이클을 처리한다
|
||||
- `fetchMissingProfilesByIds()` 사용 - 직접 `client.getProfilesByIds()` 호출 금지
|
||||
|
||||
## 다른 도메인과의 경계
|
||||
- **database 도메인**: WatermelonDB 스키마 변경 시 `docs/database/server/server.md` 업데이트 필요
|
||||
- **network 도메인**: API 호출은 `client/rest.ts`의 ClientMix 패턴 사용
|
||||
- **ui 도메인**: 화면 렌더링은 ui 도메인, 데이터 로직은 이 도메인
|
||||
|
||||
## 금지 사항
|
||||
- `console.error()` / `console.log()` 직접 사용 (→ `logError()`, `logDebug()` 사용)
|
||||
- `TouchableOpacity` 사용 (→ `Pressable` 사용)
|
||||
- DB sync handler에서 삭제 누락 (stale 레코드는 `prepareDestroyPermanently()` 처리)
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
---
|
||||
name: products-agents
|
||||
description: AI 에이전트 기능 - WebSocket 스트리밍, AI 봇, 툴 호출
|
||||
type: core
|
||||
---
|
||||
|
||||
# Products/Agents Domain
|
||||
|
||||
## 목적 / 책임
|
||||
AI 에이전트 기능을 담당한다. WebSocket 스트리밍, AI 봇 관리, 툴 호출/승인, 어노테이션을 처리한다.
|
||||
|
||||
## 포함 경로
|
||||
- `app/products/agents/` (전체)
|
||||
- `app/products/agents/actions/`
|
||||
- `app/products/agents/client/rest.ts`
|
||||
- `app/products/agents/components/`
|
||||
- `app/products/agents/constants.ts`
|
||||
- `app/products/agents/hooks/`
|
||||
- `app/products/agents/screens/`
|
||||
- `app/products/agents/store/`
|
||||
- `app/products/agents/types/`
|
||||
- `app/products/agents/websocket/`
|
||||
|
||||
## 제외 경로
|
||||
- `app/actions/remote/` (messaging 도메인)
|
||||
- `app/products/calls/` (calls 도메인)
|
||||
|
||||
## 주요 구성 요소
|
||||
- **WebSocket Handler**: `custom_mattermost-ai_postupdate` 이벤트 처리 (start, message, reasoning, tool_call, annotation, end)
|
||||
- **Streaming Store**: 스트리밍 중 ephemeral 상태 관리
|
||||
- **AI Bot**: AI 봇 목록 조회 및 DB 동기화 (`handleAIBots`)
|
||||
- **Tool Call**: 툴 호출 승인/거절 처리
|
||||
- **Client (ClientMix)**: `app/products/agents/client/rest.ts`
|
||||
|
||||
## 유지할 패턴
|
||||
- **WebSocket 스트리밍**: `ENDED` 이벤트에서 로컬 스트리밍 상태를 반드시 초기화 (stale state 방지)
|
||||
- **ClientMix 패턴**: API 호출은 `client/rest.ts` 내에서만 수행, action에서 직접 `doFetch()` 금지
|
||||
- **PostType 추가 시**: `types/api/posts.d.ts`의 `PostType` union에 타입 추가
|
||||
- **Sync handler**: create/update/delete 모두 처리 (`prepareDestroyPermanently()` 필수)
|
||||
|
||||
## 다른 도메인과의 경계
|
||||
- **messaging 도메인**: 포스트 데이터는 `POST_EDITED` WebSocket 이벤트로 수신
|
||||
- **database 도메인**: Agents 전용 DB 모델 사용
|
||||
- **network 도메인**: ClientMix를 통해 NetworkManager와 연결
|
||||
|
||||
## 금지 사항
|
||||
- 툴 호출 상태 변경을 별도 WebSocket 이벤트로 처리 (→ `POST_EDITED`로 수신)
|
||||
- `ENDED` 이벤트 후 스트리밍 상태 미초기화
|
||||
- action 파일에서 직접 `client.doFetch()` 호출
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
---
|
||||
name: products-calls
|
||||
description: 음성/화상 통화 기능
|
||||
type: core
|
||||
---
|
||||
|
||||
# Products/Calls Domain
|
||||
|
||||
## 목적 / 책임
|
||||
음성/화상 통화 기능을 담당한다. 통화 연결, 상태 관리, UI를 처리한다.
|
||||
|
||||
## 포함 경로
|
||||
- `app/products/calls/` (전체)
|
||||
- `app/products/calls/actions/`
|
||||
- `app/products/calls/client/rest.ts`
|
||||
- `app/products/calls/components/`
|
||||
- `app/products/calls/constants.ts`
|
||||
- `app/products/calls/hooks/`
|
||||
- `app/products/calls/screens/`
|
||||
- `app/products/calls/store/`
|
||||
- `app/products/calls/types/`
|
||||
|
||||
## 제외 경로
|
||||
- `app/products/agents/` (agents 도메인)
|
||||
- `app/products/playbooks/` (playbooks 도메인)
|
||||
|
||||
## 주요 구성 요소
|
||||
- **Call Connection**: WebRTC 기반 통화 연결 관리
|
||||
- **Call State**: Ephemeral store로 통화 상태 관리
|
||||
- **Client (ClientMix)**: `app/products/calls/client/rest.ts`
|
||||
|
||||
## 유지할 패턴
|
||||
- **ClientMix 패턴**: API 호출은 `client/rest.ts` 내에서만 수행
|
||||
- **Ephemeral Store**: 통화 상태는 ephemeral store로 관리 (DB 아님)
|
||||
|
||||
## 다른 도메인과의 경계
|
||||
- **messaging 도메인**: 채널 정보는 messaging 도메인 쿼리 사용
|
||||
- **network 도메인**: ClientMix를 통해 NetworkManager와 연결
|
||||
|
||||
## 금지 사항
|
||||
- action 파일에서 직접 `client.doFetch()` 호출
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
---
|
||||
name: ui
|
||||
description: 컴포넌트, 스크린, 네비게이션, 스타일링
|
||||
type: generic
|
||||
---
|
||||
|
||||
# UI Domain
|
||||
|
||||
## 목적 / 책임
|
||||
React Native UI 컴포넌트, 스크린, 네비게이션, 스타일링을 담당한다.
|
||||
|
||||
## 포함 경로
|
||||
- `app/components/` - 재사용 컴포넌트
|
||||
- `app/screens/` - 화면 단위 컴포넌트
|
||||
- `app/hooks/` - 공통 React hooks
|
||||
- `app/utils/` - UI 유틸리티
|
||||
- `share_extension/` - Android 공유 확장 (React Navigation 사용)
|
||||
|
||||
## 제외 경로
|
||||
- `app/products/*/components/` (각 product 도메인)
|
||||
- `app/actions/` (비즈니스 로직은 messaging/product 도메인)
|
||||
|
||||
## 주요 구성 요소
|
||||
- **Navigation**: `react-native-navigation` v7 (메인 앱), `React Navigation` (share extension)
|
||||
- **Components**: 60+ 재사용 컴포넌트
|
||||
- **Screens**: 63+ 화면 컴포넌트 (각각 독립 등록)
|
||||
- **Themes**: `PREFERENCES.THEMES` 상수 사용
|
||||
|
||||
## 유지할 패턴
|
||||
- `Pressable` 사용 (→ `TouchableOpacity` 금지), 항상 pressed 피드백 추가
|
||||
- `typography()` 사용 (→ raw `fontSize`/`fontWeight`/`fontFamily` 금지)
|
||||
- `makeStyleSheetFromTheme` 사용 시 `StyleSheet.create` 불필요
|
||||
- `getStyleSheet` 는 파일 상단(imports 이후)에 위치
|
||||
- 정적 객체(hitSlop 등)는 모듈 레벨 상수로 추출
|
||||
- `FormattedText` 사용 (→ `<Text>{intl.formatMessage(...)}</Text>` 지양)
|
||||
- `useServerUrl()` hook 사용 (→ `serverUrl` prop 전달 지양)
|
||||
- `usePreventDoubleTap` hook 사용 (버튼 중복 클릭 방지)
|
||||
- 리스트 렌더링: 인라인 화살표 함수 금지, 자식 컴포넌트에서 ID 받아 처리
|
||||
- `useDidMount` (→ `useEffect(cb, [])` 대신), `useInitialValue` (→ `useMemo(f, [])` 대신)
|
||||
|
||||
## 다른 도메인과의 경계
|
||||
- **messaging/product 도메인**: UI는 데이터 표시만, 비즈니스 로직은 해당 도메인
|
||||
- **database 도메인**: `observe*` 쿼리를 통해 반응형 데이터 구독
|
||||
|
||||
## 금지 사항
|
||||
- `TouchableOpacity` 사용
|
||||
- raw `fontSize`, `fontWeight`, `fontFamily` 스타일 직접 사용
|
||||
- `Object.hasOwn()` 사용 (ES2022+, RN 미지원 → `'key' in obj` 사용)
|
||||
- `Animated` API 사용 (→ `react-native-reanimated` 사용)
|
||||
- `new URL()` 사용 (→ `urlParse` 사용)
|
||||
- `Linking.openURL()` 직접 사용 (→ `tryOpenURL()` 사용)
|
||||
- `ActivityIndicator` 직접 사용 (→ `<Loading>` 컴포넌트 사용)
|
||||
- 커스텀 React 컴포넌트를 Markdown 컴포넌트 내부에 인라인 삽입
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
---
|
||||
name: global
|
||||
description: 전 프로젝트 공통 규칙
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Global Rules
|
||||
|
||||
## 응답 언어
|
||||
- 한국어로 응답한다.
|
||||
|
||||
## 기존 구조 우선
|
||||
- 새 파일 생성보다 기존 파일 수정을 우선한다.
|
||||
- 기존 패턴과 컨벤션을 따른다.
|
||||
|
||||
## 코드 변경 원칙
|
||||
- 코드 변경 전 관련 domain rule을 확인한다.
|
||||
- 요청 범위를 넘는 변경을 금지한다.
|
||||
- 불확실하면 단정하지 말고 후보로 제시한다.
|
||||
|
||||
## 작업 전 확인
|
||||
- `agent-ops/skills/router.md` 를 먼저 참조하여 필요한 skill 또는 domain rule만 로드한다.
|
||||
- 관련 rule / skill 만 최소로 로드한다.
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
---
|
||||
name: <skill-name>
|
||||
description: <skill 한 줄 설명>
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# <Skill Name>
|
||||
|
||||
## 목적
|
||||
<이 skill이 해결하는 문제>
|
||||
|
||||
## 언제 호출할지
|
||||
- <이 skill을 사용해야 하는 상황>
|
||||
- <이 skill을 사용해야 하는 상황>
|
||||
|
||||
## 입력
|
||||
- `<param>`: <설명> (필수/선택)
|
||||
|
||||
## 먼저 확인할 것
|
||||
- [ ] <사전 확인 항목>
|
||||
- [ ] <사전 확인 항목>
|
||||
|
||||
## 실행 절차
|
||||
1. <단계>
|
||||
2. <단계>
|
||||
3. <단계>
|
||||
|
||||
## 출력 형식
|
||||
```
|
||||
<출력 예시>
|
||||
```
|
||||
|
||||
## 금지 사항
|
||||
- <하면 안 되는 것>
|
||||
- <하면 안 되는 것>
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
---
|
||||
name: code-change
|
||||
description: 코드 변경 절차 - 읽기 → 도메인 rule 확인 → 변경 → 검증
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Code Change
|
||||
|
||||
## 목적
|
||||
코드 변경 시 도메인 규칙을 준수하고 기존 패턴을 따른다.
|
||||
|
||||
## 언제 호출할지
|
||||
- 새 기능을 추가할 때
|
||||
- 버그를 수정할 때
|
||||
- 기존 코드를 수정할 때
|
||||
|
||||
## 입력
|
||||
- `변경 대상`: 파일 경로 또는 기능 설명 (필수)
|
||||
- `변경 내용`: 무엇을 어떻게 변경할지 (필수)
|
||||
|
||||
## 먼저 확인할 것
|
||||
- [ ] `find-domain` skill로 대상 도메인 판별
|
||||
- [ ] 해당 domain rule 로드 (`agent-ops/rules/domain/<domain>/rules.md`)
|
||||
- [ ] 변경할 파일을 Read tool로 먼저 읽기
|
||||
- [ ] 기존 패턴과 컨벤션 파악
|
||||
|
||||
## 실행 절차
|
||||
1. 대상 파일 읽기 (Read tool 사용)
|
||||
2. 도메인 rule의 "유지할 패턴"과 "금지 사항" 확인
|
||||
3. 변경 범위를 요청 범위로 제한 (추가 리팩터링 금지)
|
||||
4. 변경 적용 (Edit tool 우선, 전면 재작성은 Write tool)
|
||||
5. TypeScript 타입 확인 (`npm run tsc`)
|
||||
6. 린팅 확인 (`npm run fix`)
|
||||
|
||||
## 출력 형식
|
||||
변경 후:
|
||||
```
|
||||
변경 파일: <path>
|
||||
변경 내용: <요약>
|
||||
확인 필요: <있으면 명시>
|
||||
```
|
||||
|
||||
## 금지 사항
|
||||
- 파일을 읽기 전에 변경하지 않는다
|
||||
- 요청 범위를 넘는 리팩터링, 주석 추가, 타입 어노테이션 추가
|
||||
- 도메인 rule의 금지 사항 위반
|
||||
- `console.log()` / `console.error()` 직접 사용
|
||||
- 다른 언어 파일 수정 (`en.json` 외 i18n 파일 수정 금지)
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
---
|
||||
name: create-domain-rule
|
||||
description: 새로운 도메인 rules.md 파일을 생성하기 위한 범용 스킬
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Create Domain Rule
|
||||
|
||||
## 목적
|
||||
|
||||
`agent-ops/rules/domain/<domain-name>/rules.md` 파일을 올바른 형식으로 생성한다.
|
||||
실제 프로젝트 폴더 구조를 분석하여 경로와 구성 요소를 자동으로 채운다.
|
||||
생성 후 router.md 의 코드 변경 라우팅 테이블에 항목을 추가한다.
|
||||
|
||||
## 언제 호출할지
|
||||
|
||||
- 새로운 도메인 영역의 코드를 처음 변경하기 전에
|
||||
- 기존 domain rule이 없는 경로에 대해 규칙이 필요할 때
|
||||
- 사용자가 특정 도메인의 rule 파일을 만들어 달라고 요청할 때
|
||||
- agent-ops 초기 scaffold 이후 도메인을 추가 확장할 때
|
||||
|
||||
## 입력
|
||||
|
||||
- `domain-name`: 생성할 도메인 이름, kebab-case (필수)
|
||||
- `domain-type`: `core` / `supporting` / `generic` 중 하나 (필수)
|
||||
- `path-hints`: 이 도메인에 해당하는 경로 힌트 목록 (선택, 없으면 자동 탐색)
|
||||
|
||||
## 먼저 확인할 것
|
||||
|
||||
- [ ] `agent-ops/rules/domain/<domain-name>/rules.md` 가 이미 존재하는지 확인
|
||||
- [ ] `agent-ops/rules/_templates/domain-rule-template.md` 를 읽어 최신 템플릿 형식 파악
|
||||
- [ ] `agent-ops/rules/domain/` 하위 기존 domain rule 목록을 확인하여 중복·유사 도메인 여부 판단
|
||||
|
||||
## 실행 절차
|
||||
|
||||
1. **중복 확인**
|
||||
- `agent-ops/rules/domain/<domain-name>/` 폴더 존재 여부 확인
|
||||
- 책임이 겹치는 기존 domain rule 이 있으면 사용자에게 알리고 중단한다
|
||||
|
||||
2. **경로 탐색**
|
||||
- `path-hints` 가 제공된 경우: 해당 경로를 기준으로 폴더 구조 확인
|
||||
- `path-hints` 가 없는 경우: `domain-name` 과 연관된 폴더명·파일명으로 프로젝트를 탐색
|
||||
- 실제로 존재하는 경로만 포함 경로에 기재한다
|
||||
- 존재하지 않는 경로는 기재하지 않는다
|
||||
|
||||
3. **도메인 분석**
|
||||
- 탐색된 경로의 주요 파일·모듈을 읽어 아래를 도출한다
|
||||
- 도메인의 목적 / 책임 한 줄 요약
|
||||
- 포함 경로 목록
|
||||
- 제외 경로 (인접 도메인과 겹칠 수 있는 경로)
|
||||
- 주요 구성 요소 (파일, 모듈, 클래스, 함수 등)
|
||||
- 유지할 패턴 (네이밍 규칙, 아키텍처 패턴 등)
|
||||
- 다른 도메인과의 경계
|
||||
- 금지 사항
|
||||
|
||||
4. **rules.md 생성**
|
||||
- 경로: `agent-ops/rules/domain/<domain-name>/rules.md`
|
||||
- `domain-rule-template.md` 형식을 따른다
|
||||
- 실제 코드에서 확인된 내용만 기재한다
|
||||
- 불확실한 항목은 `<!-- TODO: 확인 필요 -->` 주석으로 남긴다
|
||||
|
||||
5. **router.md 업데이트**
|
||||
- `agent-ops/skills/router.md` 를 읽는다
|
||||
- 코드 변경 섹션의 경로-도메인 라우팅 테이블에 아래 형식으로 추가한다
|
||||
```
|
||||
| `<path-pattern>` | `agent-ops/rules/domain/<domain-name>/rules.md` |
|
||||
```
|
||||
- 경로 패턴은 실제 포함 경로를 기준으로 작성한다
|
||||
- 기존 테이블 항목을 삭제하거나 재정렬하지 않는다
|
||||
|
||||
6. **결과 보고**
|
||||
- 생성한 파일 경로
|
||||
- router.md 에 추가한 항목
|
||||
- 불확실하여 TODO로 남긴 항목 (해당 시)
|
||||
|
||||
## 출력 형식
|
||||
|
||||
```
|
||||
## 생성 완료
|
||||
|
||||
- Domain Rule 경로: agent-ops/rules/domain/<domain-name>/rules.md
|
||||
- 도메인 유형: <core | supporting | generic>
|
||||
- router.md 추가 경로 패턴: <path-pattern>
|
||||
|
||||
## TODO 항목 (확인 필요)
|
||||
- <불확실하여 직접 확인이 필요한 항목> (해당 시)
|
||||
```
|
||||
|
||||
## 금지 사항
|
||||
|
||||
- 이미 존재하는 rules.md 를 덮어쓰지 않는다
|
||||
- 실제 존재하지 않는 경로를 포함 경로에 넣지 않는다
|
||||
- 추측으로 패턴이나 금지 사항을 채우지 않는다 — 확인된 내용만 기재한다
|
||||
- router.md 의 기존 항목을 삭제하거나 재정렬하지 않는다
|
||||
- 하나의 domain rule 에 여러 독립 도메인의 책임을 묶지 않는다
|
||||
- 코드 변경을 수행하지 않는다 — 이 skill 은 rule 파일 생성만 담당한다
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
---
|
||||
name: create-skill
|
||||
description: 새로운 SKILL.md 파일을 생성하기 위한 범용 스킬
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Create Skill
|
||||
|
||||
## 목적
|
||||
|
||||
`agent-ops/skills/<skill-name>/SKILL.md` 파일을 올바른 형식으로 생성한다.
|
||||
기존 skill-template.md 를 기반으로, 요청 목적에 맞는 내용을 채워 넣는다.
|
||||
생성 후 router.md 에 라우팅 항목을 추가한다.
|
||||
|
||||
## 언제 호출할지
|
||||
|
||||
- 새로운 반복 작업 패턴이 생겨 skill로 정의해야 할 때
|
||||
- 기존 skill이 없는 작업 유형을 처음 수행하기 전에
|
||||
- 사용자가 특정 작업을 skill로 만들어 달라고 요청할 때
|
||||
|
||||
## 입력
|
||||
|
||||
- `skill-name`: 생성할 skill 이름, kebab-case (필수)
|
||||
- `purpose`: 이 skill이 해결하는 문제 한 줄 요약 (필수)
|
||||
- `trigger-cases`: 이 skill을 호출해야 하는 상황 목록 (선택)
|
||||
|
||||
## 먼저 확인할 것
|
||||
|
||||
- [ ] `agent-ops/skills/` 하위에 동일 이름의 디렉터리가 이미 존재하는지 확인
|
||||
- [ ] `agent-ops/skills/router.md` 에 이미 유사한 라우팅 항목이 있는지 확인
|
||||
- [ ] `agent-ops/skills/_templates/skill-template.md` 를 읽어 최신 템플릿 형식 파악
|
||||
|
||||
## 실행 절차
|
||||
|
||||
1. **중복 확인**
|
||||
- `agent-ops/skills/<skill-name>/` 폴더 존재 여부 확인
|
||||
- 기능이 겹치는 기존 skill이 있으면 사용자에게 알리고 중단한다
|
||||
|
||||
2. **목적 분석**
|
||||
- `purpose` 와 `trigger-cases` 를 바탕으로 아래 항목을 도출한다
|
||||
- 언제 호출할지 (2~4개)
|
||||
- 필요한 입력 파라미터
|
||||
- 사전 확인 항목
|
||||
- 실행 절차 (3~7단계)
|
||||
- 출력 형식
|
||||
- 금지 사항
|
||||
|
||||
3. **SKILL.md 생성**
|
||||
- 경로: `agent-ops/skills/<skill-name>/SKILL.md`
|
||||
- `skill-template.md` 형식을 따른다
|
||||
- 프로젝트 특화 내용보다 범용 절차를 우선한다
|
||||
- 절차는 구체적이되 지나치게 세부 구현을 기술하지 않는다
|
||||
|
||||
4. **router.md 업데이트**
|
||||
- `agent-ops/skills/router.md` 를 읽는다
|
||||
- 이 skill이 속할 라우팅 축(구조 분석/코드 변경/흐름 추적 등)을 판단한다
|
||||
- 해당 축에 `→ agent-ops/skills/<skill-name>/SKILL.md` 항목을 추가한다
|
||||
- 기존 라우팅 구조를 깨지 않는다
|
||||
|
||||
5. **결과 보고**
|
||||
- 생성한 파일 경로
|
||||
- router.md 에 추가한 항목
|
||||
- 이 skill이 다루지 않는 범위(필요 시)
|
||||
|
||||
## 출력 형식
|
||||
|
||||
```
|
||||
## 생성 완료
|
||||
|
||||
- SKILL 경로: agent-ops/skills/<skill-name>/SKILL.md
|
||||
- router.md 추가 항목: <라우팅 축> → <skill-name>
|
||||
|
||||
## 주의사항 (해당 시)
|
||||
- <이 skill이 다루지 않는 범위 또는 주의할 점>
|
||||
```
|
||||
|
||||
## 금지 사항
|
||||
|
||||
- 이미 존재하는 skill 을 덮어쓰지 않는다
|
||||
- 프로젝트 특화 경로(예: `app/screens/`)를 skill 본문에 하드코딩하지 않는다
|
||||
- skill 생성과 무관한 코드 파일을 수정하지 않는다
|
||||
- router.md 의 기존 항목을 삭제하거나 재정렬하지 않는다
|
||||
- 하나의 skill 에 여러 독립적인 책임을 묶지 않는다
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
---
|
||||
name: find-domain
|
||||
description: 파일 경로 또는 기능으로 해당 도메인을 판별한다
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Find Domain
|
||||
|
||||
## 목적
|
||||
변경하거나 분석할 코드가 어느 도메인에 속하는지 판별한다.
|
||||
|
||||
## 언제 호출할지
|
||||
- 어떤 파일을 수정해야 하는지 불분명할 때
|
||||
- 기능이 어느 도메인에 속하는지 모를 때
|
||||
- 여러 도메인에 걸친 변경이 필요한지 파악할 때
|
||||
|
||||
## 입력
|
||||
- `경로 또는 기능 설명`: 파일 경로, 컴포넌트 이름, 또는 기능 설명 (필수)
|
||||
|
||||
## 먼저 확인할 것
|
||||
- [ ] `agent-ops/skills/router.md`의 경로 패턴 테이블 확인
|
||||
- [ ] CLAUDE.md의 TypeScript Path Aliases 섹션 확인
|
||||
|
||||
## 실행 절차
|
||||
1. 입력이 파일 경로면 → router.md 테이블과 매칭
|
||||
2. 입력이 기능 설명이면 → 관련 파일을 Glob/Grep으로 탐색
|
||||
3. 도메인 후보를 식별하고 해당 domain rule 로드
|
||||
4. 여러 도메인에 걸친 경우 → 주 도메인 + 영향 도메인 명시
|
||||
|
||||
## 출력 형식
|
||||
```
|
||||
도메인: <domain-name>
|
||||
근거: <판별 근거>
|
||||
관련 rule: agent-ops/rules/domain/<domain>/rules.md
|
||||
영향 도메인: <있으면 명시>
|
||||
```
|
||||
|
||||
## 금지 사항
|
||||
- 도메인을 단정하지 않고 불확실하면 후보를 제시한다
|
||||
- 관련 없는 domain rule까지 로드하지 않는다
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
---
|
||||
name: router
|
||||
description: 작업 유형별 skill / domain rule 라우팅
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Skill Router
|
||||
|
||||
작업 유형에 따라 필요한 skill 또는 domain rule만 로드한다.
|
||||
|
||||
## 라우팅 축
|
||||
|
||||
### 구조 분석 / 스캐폴드
|
||||
→ `agent-ops/skills/find-domain/SKILL.md`
|
||||
→ `agent-ops/rules/global.md`
|
||||
|
||||
### 새 Skill 생성
|
||||
→ `agent-ops/skills/create-skill/SKILL.md`
|
||||
|
||||
### 새 Domain Rule 생성
|
||||
→ `agent-ops/skills/create-domain-rule/SKILL.md`
|
||||
|
||||
### 도메인 판별
|
||||
→ `agent-ops/skills/find-domain/SKILL.md`
|
||||
|
||||
### 코드 변경
|
||||
→ `agent-ops/skills/code-change/SKILL.md`
|
||||
→ 변경 대상 경로에 해당하는 domain rule 로드
|
||||
|
||||
| 경로 패턴 | Domain Rule |
|
||||
|---|---|
|
||||
| `app/products/agents/` | `agent-ops/rules/domain/products-agents/rules.md` |
|
||||
| `app/products/calls/` | `agent-ops/rules/domain/products-calls/rules.md` |
|
||||
| `app/database/`, `app/queries/` | `agent-ops/rules/domain/database/rules.md` |
|
||||
| `app/components/`, `app/screens/`, `app/hooks/` | `agent-ops/rules/domain/ui/rules.md` |
|
||||
| `app/actions/`, 채널/포스트/스레드 관련 | `agent-ops/rules/domain/messaging/rules.md` |
|
||||
|
||||
### 흐름 추적
|
||||
→ 흐름에 관련된 domain rule 로드
|
||||
→ `agent-ops/skills/find-domain/SKILL.md`로 경로 판별 후 진행
|
||||
|
||||
### 문서 / 릴리즈 정리
|
||||
→ `agent-ops/rules/global.md`
|
||||
→ `docs/` 폴더 직접 확인
|
||||
|
||||
## 원칙
|
||||
- 필요한 rule만 로드한다. 전체를 로드하지 않는다.
|
||||
- 불확실한 도메인은 `find-domain` skill로 먼저 판별한다.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
apply plugin: "com.android.application"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
|
|
@ -9,14 +9,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 +47,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 +67,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,9 +98,8 @@ def reactNativeArchitectures() {
|
|||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
namespace "com.tokilabs.mattermost"
|
||||
namespace "com.mattermost.rnbeta"
|
||||
|
||||
lintOptions {
|
||||
checkReleaseBuilds false
|
||||
|
|
@ -112,11 +107,11 @@ android {
|
|||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.tokilabs.mattermost"
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 730
|
||||
versionName "2.38.0"
|
||||
versionCode 500
|
||||
versionName "2.11.0"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
|
@ -141,7 +136,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())
|
||||
}
|
||||
}
|
||||
|
|
@ -170,7 +165,6 @@ android {
|
|||
matchingFallbacks = ['release']
|
||||
}
|
||||
}
|
||||
|
||||
// applicationVariants are e.g. debug, release
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
|
|
@ -190,67 +184,65 @@ 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.1.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.6.4'
|
||||
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.6.1'
|
||||
implementation 'com.google.android.material:material:1.8.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
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')
|
||||
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: '18.1.0'
|
||||
}
|
||||
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: '18.0.2'
|
||||
}
|
||||
if (details.requested.name == 'play-services-stats') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '17.0.3'
|
||||
}
|
||||
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: '18.1.0'
|
||||
}
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,29 +1,104 @@
|
|||
{
|
||||
"project_info": {
|
||||
"project_number": "1047648748539",
|
||||
"project_id": "mattermost-6ac08",
|
||||
"storage_bucket": "mattermost-6ac08.firebasestorage.app"
|
||||
"project_number": "184930218130",
|
||||
"firebase_url": "https://api-7231322553409637977-752355.firebaseio.com",
|
||||
"project_id": "api-7231322553409637977-752355",
|
||||
"storage_bucket": "api-7231322553409637977-752355.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:1047648748539:android:818bf70bfbb3f9d070415e",
|
||||
"mobilesdk_app_id": "1:184930218130:android:a0e553d1a1f043b5",
|
||||
"android_client_info": {
|
||||
"package_name": "com.tokilabs.mattermost"
|
||||
"package_name": "com.mattermost.react.native"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "184930218130-8nahrspll1opff0uogtkh2qsv8coqngo.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyAW6j_oPl9MVcm93qS3JgaEPD5ywp-TzZ0"
|
||||
"current_key": "AIzaSyCkZkU2ECVg-mmdCG5OoHHQXtKiENuvWPE"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 2
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
"other_platform_oauth_client": []
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:184930218130:android:c7debfa7ea3f75a7",
|
||||
"android_client_info": {
|
||||
"package_name": "com.mattermost.rnbeta"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "184930218130-8nahrspll1opff0uogtkh2qsv8coqngo.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCkZkU2ECVg-mmdCG5OoHHQXtKiENuvWPE"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 2
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
"other_platform_oauth_client": []
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:184930218130:android:2db4058a5b5d6506",
|
||||
"android_client_info": {
|
||||
"package_name": "com.mattermost.rn"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "184930218130-8nahrspll1opff0uogtkh2qsv8coqngo.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCkZkU2ECVg-mmdCG5OoHHQXtKiENuvWPE"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 2
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
"other_platform_oauth_client": []
|
||||
},
|
||||
"ads_service": {
|
||||
"status": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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.* { *; }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tokilabs.mattermost;
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.wix.detox.Detox;
|
||||
import com.wix.detox.config.DetoxConfig;
|
||||
|
|
@ -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,63 @@
|
|||
/**
|
||||
* 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 com.mattermost.networkclient.RCTOkHttpClientFactory;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
RCTOkHttpClientFactory.Companion.setFlipperPlugin(networkFlipperPlugin);
|
||||
NetworkingModule.setCustomClientBuilder(
|
||||
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(
|
||||
() -> client.addPlugin(new FrescoFlipperPlugin()));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,19 @@
|
|||
<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 +33,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 +48,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"
|
||||
|
|
@ -77,7 +70,7 @@
|
|||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="mmauth_tokilabs" />
|
||||
<data android:scheme="mmauthbeta" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service android:name=".NotificationDismissService"
|
||||
|
|
@ -90,12 +83,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"
|
||||
|
|
@ -110,20 +105,6 @@
|
|||
</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"
|
||||
/>
|
||||
|
||||
<service android:name="com.voximplant.foregroundservice.VIForegroundService"/>
|
||||
</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
|
||||
|
|
@ -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,7 +19,7 @@ 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;
|
||||
|
|
@ -28,36 +28,25 @@ import androidx.core.app.Person;
|
|||
import androidx.core.app.RemoteInput;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
|
||||
import com.tokilabs.mattermost.*;
|
||||
import com.mattermost.rnutils.helpers.NotificationHelper;
|
||||
import com.nozbe.watermelondb.WMDatabase;
|
||||
import com.mattermost.turbolog.TurboLog;
|
||||
import com.mattermost.rnbeta.*;
|
||||
import com.nozbe.watermelondb.Database;
|
||||
|
||||
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";
|
||||
|
|
@ -92,7 +81,7 @@ 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);
|
||||
if (avatar != null) {
|
||||
|
|
@ -144,7 +133,7 @@ public class CustomPushNotificationHelper {
|
|||
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);
|
||||
boolean canReply = bundle.containsKey("category") && bundle.getString("category").equals(CATEGORY_CAN_REPLY);
|
||||
|
||||
if (android.text.TextUtils.isEmpty(postId) || serverUrl == null || !canReply) {
|
||||
return;
|
||||
|
|
@ -192,9 +181,9 @@ 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);
|
||||
|
|
@ -238,154 +227,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);
|
||||
|
|
@ -434,7 +275,7 @@ 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);
|
||||
if (avatar != null) {
|
||||
|
|
@ -572,12 +413,12 @@ public class CustomPushNotificationHelper {
|
|||
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);
|
||||
Database db = getDatabaseForServer(dbHelper, context, serverUrl);
|
||||
if (db != null) {
|
||||
lastUpdateAt = getLastPictureUpdate(db, userId);
|
||||
if (lastUpdateAt == null) {
|
||||
|
|
@ -594,7 +435,7 @@ public class CustomPushNotificationHelper {
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,17 @@
|
|||
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 com.nozbe.watermelondb.Database
|
||||
|
||||
import java.lang.Exception
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
typealias QueryArgs = Array<Any?>
|
||||
|
||||
class DatabaseHelper {
|
||||
var defaultDatabase: WMDatabase? = null
|
||||
var defaultDatabase: Database? = null
|
||||
|
||||
val onlyServerUrl: String?
|
||||
get() {
|
||||
|
|
@ -43,7 +39,7 @@ class DatabaseHelper {
|
|||
private fun setDefaultDatabase(context: Context) {
|
||||
val databaseName = "app.db"
|
||||
val databasePath = Uri.fromFile(context.filesDir).toString() + "/" + databaseName
|
||||
defaultDatabase = WMDatabase.getInstance(databasePath, context)
|
||||
defaultDatabase = Database.getInstance(databasePath, context)
|
||||
}
|
||||
|
||||
internal fun JSONObject.toMap(): Map<String, Any?> = keys().asSequence().associateWith { it ->
|
||||
|
|
@ -77,15 +73,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 = (APIClientModule.context == null) ? new ReactApplicationContext(context) : APIClientModule.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,29 +2,31 @@ 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 com.mattermost.helpers.database_extension.*
|
||||
import com.mattermost.helpers.push_notification.*
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
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 coroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
fun fetchAndStoreDataForPushNotification(initialData: Bundle, isReactInit: Boolean): Bundle? {
|
||||
var result: Bundle? = null
|
||||
val job = coroutineScope.launch(Dispatchers.Default) {
|
||||
result = PushNotificationDataRunnable.start(context, initialData, isReactInit)
|
||||
}
|
||||
runBlocking {
|
||||
job.join()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -35,8 +37,8 @@ class PushNotificationDataRunnable {
|
|||
private val mutex = Mutex()
|
||||
|
||||
suspend fun start(context: Context, initialData: Bundle, isReactInit: Boolean): Bundle? {
|
||||
// for more info see: https://blog.danlew.net/2020/01/28/coroutines-and-java-synchronization-dont-mix/
|
||||
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
|
||||
|
|
@ -48,40 +50,32 @@ class PushNotificationDataRunnable {
|
|||
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")
|
||||
|
||||
TurboLog.i("ReactNative", "Start fetching notification data in server=$serverUrl for channel=$channelId and ack=$ackId")
|
||||
Log.i("ReactNative", "Start fetching notification data in server=$serverUrl for channel=$channelId")
|
||||
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
val notificationData = Arguments.createMap()
|
||||
|
||||
var channel: ReadableMap? = null
|
||||
var myTeam: ReadableMap? = null
|
||||
|
||||
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) }
|
||||
res.second?.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.first?.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) {
|
||||
if (!teamId.isNullOrEmpty() && notificationData.getMap("myTeam") != null) {
|
||||
// should load all categories
|
||||
val res = fetchMyTeamCategories(db, serverUrl, teamId)
|
||||
res?.let { notificationData.putMap("categories", it) }
|
||||
} else if (channel != null) {
|
||||
} else if (notificationData.getMap("channel") != null) {
|
||||
// check if the channel is in the category for the team
|
||||
val res = addToDefaultCategoryIfNeeded(db, channel)
|
||||
val res = addToDefaultCategoryIfNeeded(db, notificationData.getMap("channel")!!)
|
||||
res?.let { notificationData.putArray("categoryChannels", it) }
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +89,7 @@ class PushNotificationDataRunnable {
|
|||
|
||||
getThreadList(notificationThread, postData?.getArray("threads"))?.let {
|
||||
val threadsArray = Arguments.createArray()
|
||||
for (item in it) {
|
||||
for(item in it) {
|
||||
threadsArray.pushMap(item)
|
||||
}
|
||||
notificationData.putArray("threads", threadsArray)
|
||||
|
|
@ -111,15 +105,13 @@ class PushNotificationDataRunnable {
|
|||
dbHelper.saveToDatabase(db, notificationData, teamId, channelId, receivingThreads)
|
||||
}
|
||||
|
||||
TurboLog.i("ReactNative", "Done processing push notification=$serverUrl for channel=$channelId and ack=$ackId")
|
||||
Log.i("ReactNative", "Done processing push notification=$serverUrl for channel=$channelId")
|
||||
}
|
||||
} 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")
|
||||
Log.i("ReactNative", "DONE fetching notification data")
|
||||
}
|
||||
|
||||
return result
|
||||
|
|
@ -135,22 +127,21 @@ class PushNotificationDataRunnable {
|
|||
threadsArray.add(thread)
|
||||
}
|
||||
for(i in 0 until it.size()) {
|
||||
it.getMap(i)?.let { thread ->
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,274 @@
|
|||
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);
|
||||
boolean cacheDirExists = cacheDir.exists();
|
||||
if (!cacheDirExists) {
|
||||
cacheDirExists = cacheDir.mkdirs();
|
||||
}
|
||||
|
||||
if (cacheDirExists) {
|
||||
tmpFile = new File(cacheDir, fileName);
|
||||
boolean fileCreated = tmpFile.createNewFile();
|
||||
|
||||
if (fileCreated) {
|
||||
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
|
||||
|
||||
try (FileInputStream inputSrc = new FileInputStream(pfd.getFileDescriptor())) {
|
||||
FileChannel src = inputSrc.getChannel();
|
||||
try (FileOutputStream outputDst = new FileOutputStream(tmpFile)) {
|
||||
FileChannel dst = outputDst.getChannel();
|
||||
dst.transferFrom(src, 0, src.size());
|
||||
src.close();
|
||||
dst.close();
|
||||
}
|
||||
}
|
||||
|
||||
pfd.close();
|
||||
}
|
||||
return tmpFile.getAbsolutePath();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileOrDirectory.delete()) {
|
||||
Log.i("ReactNative", "Couldn't delete file " + fileOrDirectory.getName());
|
||||
}
|
||||
}
|
||||
|
||||
private static String sanitizeFilename(String filename) {
|
||||
if (filename == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
File f = new File(filename);
|
||||
return f.getName();
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ package com.mattermost.helpers.database_extension
|
|||
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
fun insertCategory(db: WMDatabase, category: ReadableMap) {
|
||||
fun insertCategory(db: Database, category: ReadableMap) {
|
||||
try {
|
||||
val id = category.getString("id") ?: return
|
||||
val collapsed = false
|
||||
|
|
@ -31,7 +31,7 @@ fun insertCategory(db: WMDatabase, category: ReadableMap) {
|
|||
}
|
||||
}
|
||||
|
||||
fun insertCategoryChannels(db: WMDatabase, categoryId: String, teamId: String, channelIds: ReadableArray) {
|
||||
fun insertCategoryChannels(db: Database, categoryId: String, teamId: String, channelIds: ReadableArray) {
|
||||
try {
|
||||
for (i in 0 until channelIds.size()) {
|
||||
val channelId = channelIds.getString(i)
|
||||
|
|
@ -50,41 +50,36 @@ fun insertCategoryChannels(db: WMDatabase, categoryId: String, teamId: String, c
|
|||
}
|
||||
}
|
||||
|
||||
fun insertCategoriesWithChannels(db: WMDatabase, orderCategories: ReadableMap) {
|
||||
fun insertCategoriesWithChannels(db: Database, orderCategories: ReadableMap) {
|
||||
val categories = orderCategories.getArray("categories") ?: return
|
||||
for (i in 0 until categories.size()) {
|
||||
val category = categories.getMap(i)
|
||||
category?.let { cat ->
|
||||
val id = cat.getString("id")
|
||||
val teamId = cat.getString("team_id")
|
||||
val channelIds = cat.getArray("channel_ids")
|
||||
insertCategory(db, cat)
|
||||
if (id != null && teamId != null) {
|
||||
channelIds?.let { insertCategoryChannels(db, id, teamId, it) }
|
||||
}
|
||||
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) {
|
||||
fun insertChannelToDefaultCategory(db: Database, categoryChannels: ReadableArray) {
|
||||
try {
|
||||
for (i in 0 until categoryChannels.size()) {
|
||||
categoryChannels.getMap(i)?.let { cc ->
|
||||
val id = cc.getString("id")
|
||||
val categoryId = cc.getString("category_id")
|
||||
val channelId = cc.getString("channel_id")
|
||||
if (id != null && categoryId != null && channelId != null) {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ 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 com.nozbe.watermelondb.Database
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
fun findChannel(db: WMDatabase?, channelId: String): Boolean {
|
||||
fun findChannel(db: Database?, channelId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "Channel", channelId)
|
||||
return team != null
|
||||
|
|
@ -15,7 +15,7 @@ fun findChannel(db: WMDatabase?, channelId: String): Boolean {
|
|||
return false
|
||||
}
|
||||
|
||||
fun findMyChannel(db: WMDatabase?, channelId: String): Boolean {
|
||||
fun findMyChannel(db: Database?, channelId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "MyChannel", channelId)
|
||||
return team != null
|
||||
|
|
@ -23,7 +23,7 @@ fun findMyChannel(db: WMDatabase?, channelId: String): Boolean {
|
|||
return false
|
||||
}
|
||||
|
||||
internal fun handleChannel(db: WMDatabase, channel: ReadableMap) {
|
||||
internal fun handleChannel(db: Database, channel: ReadableMap) {
|
||||
try {
|
||||
val exists = channel.getString("id")?.let { findChannel(db, it) } ?: false
|
||||
if (!exists) {
|
||||
|
|
@ -37,7 +37,7 @@ internal fun handleChannel(db: WMDatabase, channel: ReadableMap) {
|
|||
}
|
||||
}
|
||||
|
||||
internal fun DatabaseHelper.handleMyChannel(db: WMDatabase, myChannel: ReadableMap, postsData: ReadableMap?, receivingThreads: Boolean) {
|
||||
internal fun DatabaseHelper.handleMyChannel(db: Database, myChannel: ReadableMap, postsData: ReadableMap?, receivingThreads: Boolean) {
|
||||
try {
|
||||
val json = ReadableMapUtils.toJSONObject(myChannel)
|
||||
val exists = myChannel.getString("id")?.let { findMyChannel(db, it) } ?: false
|
||||
|
|
@ -71,7 +71,7 @@ internal fun DatabaseHelper.handleMyChannel(db: WMDatabase, myChannel: ReadableM
|
|||
}
|
||||
}
|
||||
|
||||
fun insertChannel(db: WMDatabase, channel: JSONObject): Boolean {
|
||||
fun insertChannel(db: Database, 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 }
|
||||
|
|
@ -104,7 +104,7 @@ fun insertChannel(db: WMDatabase, channel: JSONObject): Boolean {
|
|||
}
|
||||
}
|
||||
|
||||
fun insertChannelInfo(db: WMDatabase, channel: JSONObject) {
|
||||
fun insertChannelInfo(db: Database, 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) { "" }
|
||||
|
|
@ -123,7 +123,7 @@ fun insertChannelInfo(db: WMDatabase, channel: JSONObject) {
|
|||
}
|
||||
}
|
||||
|
||||
fun insertMyChannel(db: WMDatabase, myChanel: JSONObject): Boolean {
|
||||
fun insertMyChannel(db: Database, 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) { "" }
|
||||
|
|
@ -156,7 +156,7 @@ fun insertMyChannel(db: WMDatabase, myChanel: JSONObject): Boolean {
|
|||
}
|
||||
}
|
||||
|
||||
fun insertMyChannelSettings(db: WMDatabase, myChanel: JSONObject) {
|
||||
fun insertMyChannelSettings(db: Database, myChanel: JSONObject) {
|
||||
try {
|
||||
val id = try { myChanel.getString("id") } catch (e: JSONException) { return }
|
||||
val notifyProps = try { myChanel.getString("notify_props") } catch (e: JSONException) { return }
|
||||
|
|
@ -173,7 +173,7 @@ fun insertMyChannelSettings(db: WMDatabase, myChanel: JSONObject) {
|
|||
}
|
||||
}
|
||||
|
||||
fun insertChannelMember(db: WMDatabase, myChanel: JSONObject) {
|
||||
fun insertChannelMember(db: Database, myChanel: JSONObject) {
|
||||
try {
|
||||
val userId = queryCurrentUserId(db) ?: return
|
||||
val channelId = try { myChanel.getString("id") } catch (e: JSONException) { return }
|
||||
|
|
@ -193,7 +193,7 @@ fun insertChannelMember(db: WMDatabase, myChanel: JSONObject) {
|
|||
}
|
||||
}
|
||||
|
||||
fun updateMyChannel(db: WMDatabase, myChanel: JSONObject) {
|
||||
fun updateMyChannel(db: Database, myChanel: JSONObject) {
|
||||
try {
|
||||
val id = try { myChanel.getString("id") } catch (e: JSONException) { return }
|
||||
val msgCount = try { myChanel.getInt("message_count") } catch (e: JSONException) { 0 }
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONArray
|
||||
|
||||
internal fun insertCustomEmojis(db: WMDatabase, customEmojis: JSONArray) {
|
||||
internal fun insertCustomEmojis(db: Database, customEmojis: JSONArray) {
|
||||
for (i in 0 until customEmojis.length()) {
|
||||
try {
|
||||
val emoji = customEmojis.getJSONObject(i)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
|
||||
internal fun insertFiles(db: WMDatabase, files: JSONArray) {
|
||||
internal fun insertFiles(db: Database, files: JSONArray) {
|
||||
try {
|
||||
for (i in 0 until files.length()) {
|
||||
val file = files.getJSONObject(i)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
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
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.QueryArgs
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
import java.util.*
|
||||
import kotlin.Exception
|
||||
|
||||
internal fun DatabaseHelper.saveToDatabase(db: WMDatabase, data: ReadableMap, teamId: String?, channelId: String?, receivingThreads: Boolean) {
|
||||
internal fun DatabaseHelper.saveToDatabase(db: Database, data: ReadableMap, teamId: String?, channelId: String?, receivingThreads: Boolean) {
|
||||
db.transaction {
|
||||
val posts = data.getMap("posts")
|
||||
data.getMap("team")?.let { insertTeam(db, it) }
|
||||
|
|
@ -26,7 +26,7 @@ internal fun DatabaseHelper.saveToDatabase(db: WMDatabase, data: ReadableMap, te
|
|||
data.getArray("threads")?.let {
|
||||
val threadsArray = ArrayList<ReadableMap>()
|
||||
for (i in 0 until it.size()) {
|
||||
it.getMap(i)?.let { map -> threadsArray.add(map) }
|
||||
threadsArray.add(it.getMap(i))
|
||||
}
|
||||
handleThreads(db, threadsArray, teamId)
|
||||
}
|
||||
|
|
@ -50,14 +50,14 @@ fun DatabaseHelper.getServerUrlForIdentifier(identifier: String): String? {
|
|||
return null
|
||||
}
|
||||
|
||||
fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): WMDatabase? {
|
||||
fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): Database? {
|
||||
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)
|
||||
val databasePath = cursor.getString(0)
|
||||
return Database.getInstance(databasePath, context!!)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -67,23 +67,7 @@ fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): W
|
|||
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? {
|
||||
fun find(db: Database, tableName: String, id: String?): ReadableMap? {
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT * FROM $tableName WHERE id == ? LIMIT 1",
|
||||
|
|
@ -103,7 +87,7 @@ fun find(db: WMDatabase, tableName: String, id: String?): ReadableMap? {
|
|||
}
|
||||
}
|
||||
|
||||
fun findByColumns(db: WMDatabase, tableName: String, columnNames: Array<String>, values: QueryArgs): ReadableMap? {
|
||||
fun findByColumns(db: Database, tableName: String, columnNames: Array<String>, values: QueryArgs): ReadableMap? {
|
||||
try {
|
||||
val whereString = columnNames.joinToString(" AND ") { "$it = ?" }
|
||||
db.rawQuery(
|
||||
|
|
@ -124,7 +108,7 @@ fun findByColumns(db: WMDatabase, tableName: String, columnNames: Array<String>,
|
|||
}
|
||||
}
|
||||
|
||||
fun queryIds(db: WMDatabase, tableName: String, ids: Array<String>): List<String> {
|
||||
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 {
|
||||
|
|
@ -145,7 +129,7 @@ fun queryIds(db: WMDatabase, tableName: String, ids: Array<String>): List<String
|
|||
return list
|
||||
}
|
||||
|
||||
fun queryByColumn(db: WMDatabase, tableName: String, columnName: String, values: Array<Any?>): List<String> {
|
||||
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 {
|
||||
|
|
@ -165,7 +149,7 @@ fun queryByColumn(db: WMDatabase, tableName: String, columnName: String, values:
|
|||
return list
|
||||
}
|
||||
|
||||
fun countByColumn(db: WMDatabase, tableName: String, columnName: String, value: Any?): Int {
|
||||
fun countByColumn(db: Database, tableName: String, columnName: String, value: Any?): Int {
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT COUNT(*) FROM $tableName WHERE $columnName == ? LIMIT 1",
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ 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 com.nozbe.watermelondb.Database
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import kotlin.Exception
|
||||
|
||||
internal fun queryLastPostCreateAt(db: WMDatabase?, channelId: String): Double? {
|
||||
internal fun queryLastPostCreateAt(db: Database?, channelId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT earliest, latest FROM PostsInChannel WHERE channel_id=? ORDER BY latest DESC LIMIT 1"
|
||||
|
|
@ -35,7 +35,7 @@ internal fun queryLastPostCreateAt(db: WMDatabase?, channelId: String): Double?
|
|||
return null
|
||||
}
|
||||
|
||||
fun queryPostSinceForChannel(db: WMDatabase?, channelId: String): Double? {
|
||||
fun queryPostSinceForChannel(db: Database?, channelId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
|
||||
|
|
@ -57,7 +57,7 @@ fun queryPostSinceForChannel(db: WMDatabase?, channelId: String): Double? {
|
|||
return null
|
||||
}
|
||||
|
||||
fun queryLastPostInThread(db: WMDatabase?, rootId: String): Double? {
|
||||
fun queryLastPostInThread(db: Database?, 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"
|
||||
|
|
@ -75,7 +75,7 @@ fun queryLastPostInThread(db: WMDatabase?, rootId: String): Double? {
|
|||
return null
|
||||
}
|
||||
|
||||
internal fun insertPost(db: WMDatabase, post: JSONObject) {
|
||||
internal fun insertPost(db: Database, post: JSONObject) {
|
||||
try {
|
||||
val id = try { post.getString("id") } catch (e: JSONException) { return }
|
||||
val channelId = try { post.getString("channel_id") } catch (e: JSONException) { return }
|
||||
|
|
@ -129,7 +129,7 @@ internal fun insertPost(db: WMDatabase, post: JSONObject) {
|
|||
}
|
||||
}
|
||||
|
||||
internal fun updatePost(db: WMDatabase, post: JSONObject) {
|
||||
internal fun updatePost(db: Database, post: JSONObject) {
|
||||
try {
|
||||
val id = try { post.getString("id") } catch (e: JSONException) { return }
|
||||
val channelId = try { post.getString("channel_id") } catch (e: JSONException) { return }
|
||||
|
|
@ -182,7 +182,7 @@ internal fun updatePost(db: WMDatabase, post: JSONObject) {
|
|||
}
|
||||
}
|
||||
|
||||
fun DatabaseHelper.handlePosts(db: WMDatabase, postsData: ReadableMap?, channelId: String, receivingThreads: Boolean) {
|
||||
fun DatabaseHelper.handlePosts(db: Database, postsData: ReadableMap?, channelId: String, receivingThreads: Boolean) {
|
||||
// Posts, PostInChannel, PostInThread, Reactions, Files, CustomEmojis, Users
|
||||
try {
|
||||
if (postsData != null) {
|
||||
|
|
|
|||
|
|
@ -4,23 +4,21 @@ 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
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
|
||||
internal fun findPostInChannel(chunks: ReadableArray, earliest: Double, latest: Double): ReadableMap? {
|
||||
for (i in 0 until chunks.size()) {
|
||||
val chunk = chunks.getMap(i)
|
||||
chunk?.let {
|
||||
if (earliest >= it.getDouble("earliest") || latest <= it.getDouble("latest")) {
|
||||
return it
|
||||
}
|
||||
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? {
|
||||
internal fun insertPostInChannel(db: Database, channelId: String, earliest: Double, latest: Double): ReadableMap? {
|
||||
return try {
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
|
|
@ -43,16 +41,14 @@ internal fun insertPostInChannel(db: WMDatabase, channelId: String, earliest: Do
|
|||
}
|
||||
}
|
||||
|
||||
internal fun mergePostsInChannel(db: WMDatabase, existingChunks: ReadableArray, newChunk: ReadableMap) {
|
||||
internal fun mergePostsInChannel(db: Database, existingChunks: ReadableArray, newChunk: ReadableMap) {
|
||||
for (i in 0 until existingChunks.size()) {
|
||||
try {
|
||||
val chunk = existingChunks.getMap(i)
|
||||
chunk?.let {
|
||||
if (newChunk.getDouble("earliest") <= it.getDouble("earliest") &&
|
||||
newChunk.getDouble("latest") >= it.getDouble("latest")) {
|
||||
db.execute("DELETE FROM PostsInChannel WHERE id = ?", arrayOf(it.getString("id")))
|
||||
return
|
||||
}
|
||||
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()
|
||||
|
|
@ -60,7 +56,7 @@ internal fun mergePostsInChannel(db: WMDatabase, existingChunks: ReadableArray,
|
|||
}
|
||||
}
|
||||
|
||||
internal fun handlePostsInChannel(db: WMDatabase, channelId: String, earliest: Double, latest: Double) {
|
||||
internal fun handlePostsInChannel(db: Database, channelId: String, earliest: Double, latest: Double) {
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT id, channel_id, earliest, latest FROM PostsInChannel WHERE channel_id = ?",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.mattermost.helpers.mapCursor
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
|
||||
fun getTeammateDisplayNameSetting(db: WMDatabase): String {
|
||||
fun getTeammateDisplayNameSetting(db: Database): String {
|
||||
val configSetting = queryConfigDisplayNameSetting(db)
|
||||
if (configSetting != null) {
|
||||
return configSetting
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.mattermost.helpers.RandomId
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONArray
|
||||
|
||||
internal fun insertReactions(db: WMDatabase, reactions: JSONArray) {
|
||||
internal fun insertReactions(db: Database, reactions: JSONArray) {
|
||||
for (i in 0 until reactions.length()) {
|
||||
try {
|
||||
val reaction = reactions.getJSONObject(i)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONObject
|
||||
|
||||
fun queryCurrentUserId(db: WMDatabase): String? {
|
||||
fun queryCurrentUserId(db: Database): String? {
|
||||
val result = find(db, "System", "currentUserId")
|
||||
return result?.getString("value")?.removeSurrounding("\"")
|
||||
}
|
||||
|
||||
fun queryCurrentTeamId(db: WMDatabase): String? {
|
||||
fun queryCurrentTeamId(db: Database): String? {
|
||||
val result = find(db, "System", "currentTeamId")
|
||||
return result?.getString("value")?.removeSurrounding("\"")
|
||||
}
|
||||
|
||||
fun queryConfigDisplayNameSetting(db: WMDatabase): String? {
|
||||
fun queryConfigDisplayNameSetting(db: Database): String? {
|
||||
val license = find(db, "System", "license")
|
||||
val lockDisplayName = find(db, "Config", "LockTeammateNameDisplay")
|
||||
val displayName = find(db, "Config", "TeammateNameDisplay")
|
||||
|
|
@ -30,11 +30,3 @@ fun queryConfigDisplayNameSetting(db: WMDatabase): String? {
|
|||
|
||||
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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ 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
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
|
||||
fun findTeam(db: WMDatabase?, teamId: String): Boolean {
|
||||
fun findTeam(db: Database?, teamId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "Team", teamId)
|
||||
return team != null
|
||||
|
|
@ -14,7 +14,7 @@ fun findTeam(db: WMDatabase?, teamId: String): Boolean {
|
|||
return false
|
||||
}
|
||||
|
||||
fun findMyTeam(db: WMDatabase?, teamId: String): Boolean {
|
||||
fun findMyTeam(db: Database?, teamId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "MyTeam", teamId)
|
||||
return team != null
|
||||
|
|
@ -22,7 +22,7 @@ fun findMyTeam(db: WMDatabase?, teamId: String): Boolean {
|
|||
return false
|
||||
}
|
||||
|
||||
fun queryMyTeams(db: WMDatabase?): ArrayList<ReadableMap>? {
|
||||
fun queryMyTeams(db: Database?): ArrayList<ReadableMap>? {
|
||||
db?.rawQuery("SELECT * FROM MyTeam")?.use { cursor ->
|
||||
val results = ArrayList<ReadableMap>()
|
||||
if (cursor.count > 0) {
|
||||
|
|
@ -38,7 +38,7 @@ fun queryMyTeams(db: WMDatabase?): ArrayList<ReadableMap>? {
|
|||
return null
|
||||
}
|
||||
|
||||
fun insertTeam(db: WMDatabase, team: ReadableMap): Boolean {
|
||||
fun insertTeam(db: Database, 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) {
|
||||
|
|
@ -78,7 +78,7 @@ fun insertTeam(db: WMDatabase, team: ReadableMap): Boolean {
|
|||
}
|
||||
}
|
||||
|
||||
fun insertMyTeam(db: WMDatabase, myTeam: ReadableMap): Boolean {
|
||||
fun insertMyTeam(db: Database, 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) { "" }
|
||||
|
|
|
|||
|
|
@ -1,31 +1,15 @@
|
|||
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 com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
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) {
|
||||
internal fun insertThread(db: Database, thread: ReadableMap) {
|
||||
// These fields are not present when we extract threads from posts
|
||||
try {
|
||||
val id = try { thread.getString("id") } catch (e: NoSuchKeyException) { return }
|
||||
|
|
@ -33,7 +17,7 @@ internal fun insertThread(db: WMDatabase, thread: ReadableMap) {
|
|||
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 lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
|
|
@ -52,7 +36,7 @@ internal fun insertThread(db: WMDatabase, thread: ReadableMap) {
|
|||
}
|
||||
}
|
||||
|
||||
internal fun updateThread(db: WMDatabase, thread: ReadableMap, existingRecord: ReadableMap) {
|
||||
internal fun updateThread(db: Database, 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 }
|
||||
|
|
@ -60,7 +44,7 @@ internal fun updateThread(db: WMDatabase, thread: ReadableMap, existingRecord: R
|
|||
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 lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
|
|
@ -79,28 +63,26 @@ internal fun updateThread(db: WMDatabase, thread: ReadableMap, existingRecord: R
|
|||
}
|
||||
}
|
||||
|
||||
internal fun insertThreadParticipants(db: WMDatabase, threadId: String, participants: ReadableArray) {
|
||||
internal fun insertThreadParticipants(db: Database, threadId: String, participants: ReadableArray) {
|
||||
for (i in 0 until participants.size()) {
|
||||
try {
|
||||
val participant = participants.getMap(i)
|
||||
participant?.let {
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ThreadParticipant
|
||||
(id, thread_id, user_id, _changed, _status)
|
||||
VALUES (?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, threadId, it.getString("id"))
|
||||
)
|
||||
}
|
||||
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) {
|
||||
fun insertTeamThreadsSync(db: Database, teamId: String, earliest: Double, latest: Double) {
|
||||
try {
|
||||
val query = """
|
||||
INSERT INTO TeamThreadsSync (id, _changed, _status, earliest, latest)
|
||||
|
|
@ -112,7 +94,7 @@ fun insertTeamThreadsSync(db: WMDatabase, teamId: String, earliest: Double, late
|
|||
}
|
||||
}
|
||||
|
||||
fun updateTeamThreadsSync(db: WMDatabase, teamId: String, earliest: Double, latest: Double, existingRecord: ReadableMap) {
|
||||
fun updateTeamThreadsSync(db: Database, teamId: String, earliest: Double, latest: Double, existingRecord: ReadableMap) {
|
||||
try {
|
||||
val storeEarliest = minOf(earliest, existingRecord.getDouble("earliest"))
|
||||
val storeLatest = maxOf(latest, existingRecord.getDouble("latest"))
|
||||
|
|
@ -123,7 +105,7 @@ fun updateTeamThreadsSync(db: WMDatabase, teamId: String, earliest: Double, late
|
|||
}
|
||||
}
|
||||
|
||||
fun syncParticipants(db: WMDatabase, thread: ReadableMap) {
|
||||
fun syncParticipants(db: Database, thread: ReadableMap) {
|
||||
try {
|
||||
val threadId = thread.getString("id")
|
||||
val participants = thread.getArray("participants")
|
||||
|
|
@ -139,7 +121,7 @@ fun syncParticipants(db: WMDatabase, thread: ReadableMap) {
|
|||
}
|
||||
}
|
||||
|
||||
internal fun handlePostsInThread(db: WMDatabase, postsInThread: Map<String, List<JSONObject>>) {
|
||||
internal fun handlePostsInThread(db: Database, postsInThread: Map<String, List<JSONObject>>) {
|
||||
postsInThread.forEach { (key, list) ->
|
||||
try {
|
||||
val sorted = list.sortedBy { it.getDouble("create_at") }
|
||||
|
|
@ -179,7 +161,7 @@ internal fun handlePostsInThread(db: WMDatabase, postsInThread: Map<String, List
|
|||
}
|
||||
}
|
||||
|
||||
fun handleThreads(db: WMDatabase, threads: ArrayList<ReadableMap>, teamId: String?) {
|
||||
fun handleThreads(db: Database, threads: ArrayList<ReadableMap>, teamId: String?) {
|
||||
val teamIds = ArrayList<String>()
|
||||
if (teamId.isNullOrEmpty()) {
|
||||
val myTeams = queryMyTeams(db)
|
||||
|
|
@ -204,7 +186,7 @@ fun handleThreads(db: WMDatabase, threads: ArrayList<ReadableMap>, teamId: Strin
|
|||
handleTeamThreadsSync(db, threads, teamIds)
|
||||
}
|
||||
|
||||
fun handleThread(db: WMDatabase, thread: ReadableMap, teamIds: ArrayList<String>) {
|
||||
fun handleThread(db: Database, thread: ReadableMap, teamIds: ArrayList<String>) {
|
||||
// Insert/Update the thread
|
||||
val threadId = thread.getString("id")
|
||||
val isFollowing = thread.getBoolean("is_following")
|
||||
|
|
@ -225,7 +207,7 @@ fun handleThread(db: WMDatabase, thread: ReadableMap, teamIds: ArrayList<String>
|
|||
}
|
||||
}
|
||||
|
||||
fun handleThreadInTeam(db: WMDatabase, thread: ReadableMap, teamId: String) {
|
||||
fun handleThreadInTeam(db: Database, thread: ReadableMap, teamId: String) {
|
||||
val threadId = thread.getString("id") ?: return
|
||||
val existingRecord = findByColumns(
|
||||
db,
|
||||
|
|
@ -247,21 +229,10 @@ fun handleThreadInTeam(db: WMDatabase, thread: ReadableMap, teamId: String) {
|
|||
}
|
||||
}
|
||||
|
||||
fun handleTeamThreadsSync(db: WMDatabase, threadList: ArrayList<ReadableMap>, teamIds: ArrayList<String>) {
|
||||
fun handleTeamThreadsSync(db: Database, 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;
|
||||
}
|
||||
.sortedBy { it.getDouble("last_reply_at") }
|
||||
.map { it.getDouble("last_reply_at") }
|
||||
val earliest = sortedList.first()
|
||||
val latest = sortedList.last()
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ 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
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
fun getLastPictureUpdate(db: WMDatabase?, userId: String): Double? {
|
||||
fun getLastPictureUpdate(db: Database?, userId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
var id = userId
|
||||
|
|
@ -28,7 +28,7 @@ fun getLastPictureUpdate(db: WMDatabase?, userId: String): Double? {
|
|||
return null
|
||||
}
|
||||
|
||||
fun getCurrentUserLocale(db: WMDatabase): String {
|
||||
fun getCurrentUserLocale(db: Database): String {
|
||||
try {
|
||||
val currentUserId = queryCurrentUserId(db) ?: return "en"
|
||||
val userQuery = "SELECT locale FROM User WHERE id=?"
|
||||
|
|
@ -45,43 +45,41 @@ fun getCurrentUserLocale(db: WMDatabase): String {
|
|||
return "en"
|
||||
}
|
||||
|
||||
fun handleUsers(db: WMDatabase, users: ReadableArray) {
|
||||
fun handleUsers(db: Database, users: ReadableArray) {
|
||||
for (i in 0 until users.size()) {
|
||||
val user = users.getMap(i)
|
||||
user?.let { u ->
|
||||
val roles = u.getString("roles") ?: ""
|
||||
val isBot = try {
|
||||
u.getBoolean("is_bot")
|
||||
} catch (e: NoSuchKeyException) {
|
||||
false
|
||||
}
|
||||
val roles = user.getString("roles") ?: ""
|
||||
val isBot = try {
|
||||
user.getBoolean("is_bot")
|
||||
} catch (e: NoSuchKeyException) {
|
||||
false
|
||||
}
|
||||
|
||||
val lastPictureUpdate = try { u.getDouble("last_picture_update") } catch (e: NoSuchKeyException) { 0 }
|
||||
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(
|
||||
u.getString("id"),
|
||||
u.getString("auth_service"), u.getDouble("update_at"), u.getDouble("delete_at"),
|
||||
u.getString("email"), u.getString("first_name"), isBot,
|
||||
roles.contains("system_guest"), u.getString("last_name"), lastPictureUpdate,
|
||||
u.getString("locale"), u.getString("nickname"), u.getString("position"),
|
||||
roles, "", u.getString("username"), "{}",
|
||||
ReadableMapUtils.toJSONObject(u.getMap("props")
|
||||
?: Arguments.createMap()).toString(),
|
||||
ReadableMapUtils.toJSONObject(u.getMap("timezone")
|
||||
?: Arguments.createMap()).toString(),
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ 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
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
suspend fun PushNotificationDataRunnable.Companion.fetchMyTeamCategories(db: WMDatabase, serverUrl: String, teamId: String): ReadableMap? {
|
||||
suspend fun PushNotificationDataRunnable.Companion.fetchMyTeamCategories(db: Database, serverUrl: String, teamId: String): ReadableMap? {
|
||||
return try {
|
||||
val userId = queryCurrentUserId(db)
|
||||
val categories = fetch(serverUrl, "/api/v4/users/$userId/teams/$teamId/channels/categories")
|
||||
|
|
@ -20,7 +20,7 @@ suspend fun PushNotificationDataRunnable.Companion.fetchMyTeamCategories(db: WMD
|
|||
}
|
||||
}
|
||||
|
||||
fun PushNotificationDataRunnable.Companion.addToDefaultCategoryIfNeeded(db: WMDatabase, channel: ReadableMap): ReadableArray? {
|
||||
fun PushNotificationDataRunnable.Companion.addToDefaultCategoryIfNeeded(db: Database, channel: ReadableMap): ReadableArray? {
|
||||
val channelId = channel.getString("id") ?: return null
|
||||
val channelType = channel.getString("type")
|
||||
val categoryChannels = Arguments.createArray()
|
||||
|
|
@ -44,7 +44,7 @@ fun PushNotificationDataRunnable.Companion.addToDefaultCategoryIfNeeded(db: WMDa
|
|||
return categoryChannels
|
||||
}
|
||||
|
||||
private fun categoryChannelForTeam(db: WMDatabase, channelId: String, teamId: String?, type: String): ReadableMap? {
|
||||
private fun categoryChannelForTeam(db: Database, 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")
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue