Compare commits

..

8 commits

Author SHA1 Message Date
unified-ci-app[bot]
043d529143
Bump app build number (#8132)
Some checks failed
github-release / test (push) Has been cancelled
github-release / build-ios-unsigned (push) Has been cancelled
github-release / build-android-unsigned (push) Has been cancelled
github-release / release (push) Has been cancelled
* Bump app build number to 543

* Update build.gradle

* Update project.pbxproj

* Update Info.plist

* Update Info.plist

* Update Info.plist

---------

Co-authored-by: runner <runner@Mac-1722960938962.local>
Co-authored-by: Amy Blais <29708087+amyblais@users.noreply.github.com>
2024-08-06 12:35:57 -04:00
unified-ci-app[bot]
f877df3f0a
Bump app build number to 542 (#8127)
Co-authored-by: runner <runner@Mac-1722610887106.local>
2024-08-02 11:17:52 -04:00
Mattermost Build
21c7628da1
fix: (MM-59956) Android crash when sharing (#8122) (#8126)
(cherry picked from commit 1de7b5e9b7)

Co-authored-by: Rahim Rahman <rahim.rahman@mattermost.com>
2024-08-02 11:10:47 -04:00
unified-ci-app[bot]
48b80ef09f
Bump app build number to 539 (#8111)
Co-authored-by: runner <runner@Mac-1721909426350.local>
2024-07-25 08:38:38 -04:00
Mattermost Build
6bbbd73deb
Avoid registering wrong performance load logs (#8107) (#8108)
(cherry picked from commit 26ae247d0f)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
2024-07-24 13:07:53 -04:00
unified-ci-app[bot]
6144f04180
Bump app build and version number (#8104)
* Bump app build number to 538

* Bump app version number to 2.19.0

---------

Co-authored-by: runner <runner@Mac-1721390087484.local>
2024-07-19 08:24:24 -04:00
Mattermost Build
0bf441b49d
Fix server list bottom sheet on tablet (#8094) (#8097)
(cherry picked from commit ea8adb5167)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2024-07-16 14:52:36 -04:00
Mattermost Build
6cc87dacc6
MM-56933 - password dont disable autocomplete (#8057) (#8090)
Some checks failed
github-release / test (push) Has been cancelled
github-release / build-ios-unsigned (push) Has been cancelled
github-release / build-android-unsigned (push) Has been cancelled
github-release / release (push) Has been cancelled
(cherry picked from commit d02e39b1f3)

Co-authored-by: Pablo Vélez <pablovv2016@gmail.com>
2024-07-15 09:40:23 -04:00
2501 changed files with 46091 additions and 253848 deletions

115
.eslintrc.json Normal file
View 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|@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
}
}
]
}

View file

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

View file

@ -52,22 +52,20 @@ class Specs {
}
generateSplits() {
const chunkSize = Math.floor(this.rawFiles.length / this.parallelism);
let remainder = this.rawFiles.length % this.parallelism;
const chunkSize = Math.ceil(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(' ');
for (let i = 0; i < this.rawFiles.length; i += chunkSize) {
const end = Math.min(i + chunkSize, this.rawFiles.length);
const fileGroup = this.rawFiles.slice(i, end).join(' ');
const specFileGroup = new SpecGroup(runNo.toString(), fileGroup, this.deviceInfo);
this.groupedFiles.push(specFileGroup);
start = end;
runNo++;
if (remainder > 0) {
remainder--;
if (end === this.rawFiles.length) {
break;
}
runNo++;
}
}

View file

@ -12,13 +12,6 @@ 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:
@ -34,7 +27,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 }}

View file

@ -1,61 +1,24 @@
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/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"
npm run ios-gems
npm run pod-install
echo "::endgroup::"
- name: Cache Pods
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-

View file

@ -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

View file

@ -5,7 +5,7 @@ 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@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # v1.180.0
- name: ci/setup-fastlane-dependencies
shell: bash
@ -16,7 +16,7 @@ runs:
working-directory: ./fastlane
- name: Cache Ruby gems
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}

View file

@ -5,7 +5,7 @@ runs:
using: composite
steps:
- name: ci/setup-node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version-file: ".nvmrc"
cache: "npm"
@ -22,7 +22,7 @@ runs:
echo "::endgroup::"
- name: Cache Node.js modules
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

View file

@ -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"

View file

@ -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::"

View file

@ -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"

View file

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

View file

@ -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 }}

View file

@ -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: |

View file

@ -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"

View file

@ -8,7 +8,7 @@ on:
- build-beta-android-[0-9]+
env:
NODE_VERSION: 22.14.0
NODE_VERSION: 20.13.1
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- 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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/prepare-android-build
uses: ./.github/actions/prepare-android-build
@ -57,25 +54,7 @@ jobs:
working-directory: ./fastlane
- name: ci/upload-android-beta-build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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

View file

@ -8,7 +8,7 @@ on:
- build-release-android-[0-9]+
env:
NODE_VERSION: 22.14.0
NODE_VERSION: 20.13.1
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- 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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- 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@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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

View file

@ -9,36 +9,29 @@ on:
- build-beta-sim-[0-9]+
env:
NODE_VERSION: 22.14.0
NODE_VERSION: 20.13.1
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/test
uses: ./.github/actions/test
build-ios-simulator:
runs-on: macos-15-large
runs-on: macos-14-large
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- 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@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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-14-large
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- 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@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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

View file

@ -9,47 +9,43 @@ on:
- build-release-sim-[0-9]+
env:
NODE_VERSION: 22.14.0
NODE_VERSION: 20.13.1
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/test
uses: ./.github/actions/test
build-and-deploy-ios-release:
runs-on: macos-15-large
runs-on: macos-14-large
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- 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@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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-14-large
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- 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@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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

View file

@ -6,9 +6,8 @@ on:
- labeled
env:
NODE_VERSION: 22.14.0
NODE_VERSION: 20.13.1
TERM: xterm
INTUNE_ENABLED: 'true'
jobs:
test:
@ -16,33 +15,34 @@ jobs:
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
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: ci/test
uses: ./.github/actions/test
build-ios-pr:
runs-on: macos-15-large
runs-on: macos-14-large
if: ${{ github.event.label.name == 'Build Apps for PR' || github.event.label.name == 'Build App for iOS' }}
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
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 }}
- 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:
@ -60,7 +60,7 @@ jobs:
working-directory: ./fastlane
- name: ci/upload-ios-pr-build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: ios-build-pr-${{ github.run_id }}
path: "*.ipa"
@ -72,7 +72,7 @@ jobs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
@ -94,7 +94,7 @@ jobs:
working-directory: ./fastlane
- name: ci/upload-android-pr-build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: android-build-pr-${{ github.run_id }}
path: "*.apk"

View file

@ -7,11 +7,8 @@ on:
- 'release*'
pull_request:
permissions:
pull-requests: write
env:
NODE_VERSION: 22.14.0
NODE_VERSION: 20.13.1
TERM: xterm
jobs:
@ -19,11 +16,6 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- 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 }}

View file

@ -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

View file

@ -20,7 +20,7 @@ jobs:
run: echo '${{ inputs.CMT_MATRIX }}' > instance-details.json
- name: cmt/upload-instance-details
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: instance-details.json
path: instance-details.json
@ -32,7 +32,7 @@ jobs:
MOBILE_SHA: ${{ steps.repo.outputs.MOBILE_SHA }}
steps:
- name: cmt/checkout-mobile
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ inputs.MOBILE_VERSION }}
@ -45,7 +45,7 @@ jobs:
needs:
- calculate-commit-hash
steps:
- uses: mattermost/actions/delivery/update-commit-status@d5174b860704729f4c14ef8489ae075742bfa08a
- uses: mattermost/actions/delivery/update-commit-status@746563b58e737a17a8ceb00b84a813b9e6e1b236
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@ -74,7 +74,7 @@ jobs:
- update-initial-status
steps:
- name: Checkout Repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ inputs.MOBILE_VERSION }}
@ -91,7 +91,7 @@ jobs:
working-directory: ./fastlane
- name: Upload iOS Simulator Build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: ios-build-simulator-${{ github.run_id }}
path: Mattermost-simulator-x86_64.app.zip

View file

@ -1,6 +1,4 @@
# 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
name: Detox E2E Tests PR
on:
pull_request:
@ -9,32 +7,10 @@ on:
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')
update-initial-status:
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')
if: contains(github.event.pull_request.labels.*.name, 'E2E iOS tests for PR')
steps:
- uses: mattermost/actions/delivery/update-commit-status@main
env:
@ -42,112 +18,49 @@ jobs:
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 ...
context: e2e/detox-tests
description: Detox 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
runs-on: macos-14
needs:
- update-initial-status-ios
- update-initial-status
steps:
- name: Checkout Repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- 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 }}"
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 skip_upload_to_s3_bucket:true
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
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: ios-build-simulator-${{ github.run_id }}
path: Mattermost-simulator-*.app.zip
path: Mattermost-simulator-x86_64.app.zip
build-android-apk:
runs-on: ubuntu-latest-8-cores
if: contains(github.event.label.name, 'E2E Android tests for PR')
needs:
- update-initial-status-android
env:
ORG_GRADLE_PROJECT_jvmargs: -Xmx8g
steps:
- name: Prune Docker to free up space
run: docker system prune -af
- name: Remove npm Temporary Files
run: |
rm -rf ~/.npm/_cacache
- name: Checkout Repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Prepare Android Build
uses: ./.github/actions/prepare-android-build
env:
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
- name: Install Dependencies
run: sudo apt-get clean && sudo apt-get update && sudo apt-get install -y default-jdk
- name: Detox build
run: |
cd detox
npm install
npm install -g detox-cli
npm run e2e:android-inject-settings
npm run e2e:android-build
- name: Upload Android Build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: android-build-files-${{ github.run_id }}
path: "android/app/build/**/*"
run-ios-tests-on-pr:
if: contains(github.event.label.name, 'E2E iOS tests for PR')
name: iOS
uses: ./.github/workflows/e2e-ios-template.yml
name: iOS Mobile Tests on PR
uses: ./.github/workflows/e2e-detox-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-ios-tests: true
run-type: "PR"
MOBILE_VERSION: ${{ github.event.pull_request.head.sha }}
secrets: inherit
update-final-status-ios:
update-final-status:
runs-on: ubuntu-22.04
if: contains(github.event.label.name, 'E2E iOS tests for PR')
needs:
- run-ios-tests-on-pr
steps:
@ -157,68 +70,31 @@ jobs:
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ github.event.pull_request.head.sha }}
context: e2e/detox-ios-tests
context: e2e/detox-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:
e2e-remove-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
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # 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,
});
}
});
const labels = context.payload.pull_request.labels.map(label => label.name);
if (labels.includes(iosLabel)) {
github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: iosLabel,
});
}

View file

@ -5,11 +5,8 @@ on:
branches:
- release-*
env:
INTUNE_ENABLED: 'true'
jobs:
update-initial-status-ios:
update-initial-status:
runs-on: ubuntu-22.04
steps:
- uses: mattermost/actions/delivery/update-commit-status@main
@ -18,36 +15,20 @@ jobs:
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 ...
context: e2e/detox-tests
description: Detox tests for mattermost mobile app have started ...
status: pending
build-ios-simulator:
runs-on: macos-15
runs-on: macos-14
needs:
- update-initial-status-ios
- update-initial-status
steps:
- name: Checkout Repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- 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:
@ -59,85 +40,24 @@ jobs:
working-directory: ./fastlane
- name: Upload iOS Simulator Build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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
uses: ./.github/workflows/e2e-detox-template.yml
needs:
- build-ios-simulator
with:
run-ios-tests: true
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:
update-final-status:
runs-on: ubuntu-22.04
needs:
- run-ios-tests-on-release
@ -148,23 +68,7 @@ jobs:
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ github.sha }}
context: e2e/detox-ios-tests
context: e2e/detox-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 }}

View file

@ -4,11 +4,8 @@ on:
schedule:
- cron: "0 0 * * 4,5" # Wednesday and Thursday midnight
env:
INTUNE_ENABLED: 'true'
jobs:
update-initial-status-ios:
update-initial-status:
runs-on: ubuntu-22.04
steps:
- uses: mattermost/actions/delivery/update-commit-status@main
@ -17,36 +14,20 @@ jobs:
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 ...
context: e2e/detox-tests
description: Detox tests for mattermost mobile app have started ...
status: pending
build-ios-simulator:
runs-on: macos-15
runs-on: macos-14
needs:
- update-initial-status-ios
- update-initial-status
steps:
- name: Checkout Repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- 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:
@ -58,85 +39,24 @@ jobs:
working-directory: ./fastlane
- name: Upload iOS Simulator Build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
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
uses: ./.github/workflows/e2e-detox-template.yml
needs:
- build-ios-simulator
with:
run-ios-tests: true
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:
update-final-status:
runs-on: ubuntu-22.04
needs:
- run-ios-tests-on-main-scheduled
@ -147,23 +67,7 @@ jobs:
with:
repository_full_name: ${{ github.repository }}
commit_sha: ${{ github.sha }}
context: e2e/detox-ios-tests
context: e2e/detox-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 }}

View file

@ -1,4 +1,4 @@
name: Detox Android E2E Tests Template
name: Detox E2E Tests Template
on:
workflow_call:
@ -20,8 +20,8 @@ on:
required: false
default: ${{ github.head_ref || github.ref }}
type: string
run-android-tests:
description: "Run Android tests"
run-ios-tests:
description: "Run iOS tests"
required: true
type: boolean
run-type:
@ -38,21 +38,16 @@ on:
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"
ios_device_name:
description: "iPhone simulator name"
required: false
type: string
default: "detox_pixel_4_xl"
android_api_level:
description: "Android API level"
default: "iPhone 14"
ios_device_os_name:
description: "iPhone simulator OS version"
required: false
type: string
default: "34"
default: "iOS 17.0"
outputs:
STATUS:
value: ${{ jobs.generate-report.outputs.STATUS }}
@ -66,10 +61,10 @@ env:
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 }}
# BRANCH: ${{ 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 }}
@ -84,14 +79,11 @@ env:
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
if: ${{ inputs.run-ios-tests }}
outputs:
specs: ${{ steps.generate-specs.outputs.specs }}
build_id: ${{ steps.resolve-device.outputs.BUILD_ID }}
@ -100,14 +92,14 @@ jobs:
workflow_hash: ${{ steps.resolve-device.outputs.WORKFLOW_HASH }}
steps:
- name: Checkout Repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ inputs.MOBILE_VERSION }}
- name: Set Build ID
id: resolve-device
run: |
BUILD_ID="${{ github.run_id }}-${{ env.AVD_NAME }}-${{ env.SDK_VERSION}}"
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
@ -123,17 +115,17 @@ jobs:
with:
parallelism: 10
search_path: detox/e2e/test
device_name: ${{ env.AVD_NAME }}
device_os_version: ${{ env.SDK_VERSION }}
device_name: ${{ env.DEVICE_NAME }}
device_os_version: ${{ env.DEVICE_OS_VERSION }}
e2e-android:
name: machine-${{ matrix.runId }}-api-${{ matrix.deviceOsVersion }}
runs-on: ubuntu-latest-8-cores
e2e-ios:
name: ios-detox-e2e-${{ matrix.runId }}-${{ matrix.deviceName }}-${{ matrix.deviceOsVersion }}
if: ${{ inputs.run-ios-tests }}
runs-on: macos-14
continue-on-error: true
timeout-minutes: 240
timeout-minutes: 40
env:
ANDROID_HOME: /usr/local/lib/android/sdk
ANDROID_SDK_ROOT: /usr/local/lib/android/sdk
IOS: true
needs:
- generate-specs
strategy:
@ -141,121 +133,74 @@ jobs:
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
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ inputs.MOBILE_VERSION }}
- name: ci/prepare-node-deps
uses: ./.github/actions/prepare-node-deps
- name: Download Android Artifacts
- 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/
- name: Start React Native Metro Server
run: npm run start &
- name: Install Detox Dependencies
run: cd detox && npm i
- name: Run Detox E2E Tests
continue-on-error: true # Label might have been removed manually
run: |
cd detox
npm run detox:config-gen
npm run e2e:ios-test -- ${{ matrix.specs }}
env:
DETOX_DEVICE_TYPE: ${{ env.DEVICE_NAME }}
DETOX_OS_VERSION: ${{ env.DEVICE_OS_VERSION }}
- name: Upload iOS Test Report
if: always()
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: ios-results-${{ needs.generate-specs.outputs.workflow_hash }}-${{ matrix.runId }}
path: detox/artifacts/
generate-report:
runs-on: ubuntu-22.04
if: ${{ inputs.run-ios-tests}}
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ inputs.MOBILE_VERSION }}
- name: ci/prepare-node-deps
uses: ./.github/actions/prepare-node-deps
- name: Download All 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
pattern: ios-results-${{ needs.generate-specs.outputs.workflow_hash }}-*
- name: Generate Report Path
id: s3
@ -272,6 +217,7 @@ jobs:
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 }}
IOS: ${{ inputs.run-ios-tests }}
BUILD_ID: ${{ needs.generate-specs.outputs.build_id }}
REPORT_PATH: ${{ steps.s3.outputs.path }}
## These are needed for the MM Webhook report
@ -291,7 +237,7 @@ jobs:
- 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}
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
@ -310,36 +256,3 @@ jobs:
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 }}

View file

@ -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 }}

View file

@ -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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/test
uses: ./.github/actions/test
build-ios-unsigned:
runs-on: macos-15-large
runs-on: macos-14-large
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- 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@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: ci/prepare-android-build
uses: ./.github/actions/prepare-android-build
@ -90,133 +62,34 @@ 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@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: ruby/setup-ruby@cb0fda56a307b8c78d38320cd40d9eb22a3bf04e # v1.242.0
- uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # v1.180.0
- name: release/setup-fastlane-dependencies
run: bundle install
working-directory: ./fastlane
- name: ci/download-artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
path: ${{ github.workspace }}
merge-multiple: true
@ -226,9 +99,3 @@ jobs:
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"

View file

@ -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)"

300
.gitignore vendored
View file

@ -1,199 +1,123 @@
# 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*
libraries/**/**/build
libraries/**/**/.build
# Android sounds
android/app/src/main/res/raw/*

11
.gitmodules vendored
View file

@ -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

View file

@ -1,3 +0,0 @@
# 필수 준수 사항
- agent-ops/rules/global.md 파일을 읽고 이에 대해 꼭 준수해서 진행해야한다.
- global.md 내용을 모르면 추정하지 말고 먼저 해당 파일을 확인한다.

View file

@ -1 +0,0 @@
-

View file

@ -1 +0,0 @@
-

View file

@ -1 +0,0 @@
-

View file

@ -1 +0,0 @@
-

View file

@ -1 +0,0 @@
-

View file

@ -1 +0,0 @@
-

View file

@ -1 +1 @@
22.14.0
20.13.1

2
.nvmrc
View file

@ -1 +1 @@
22.14.0
20.13.1

View file

@ -1 +1 @@
3.2.0
3.0.6

View file

@ -32,14 +32,14 @@
{
"rule": "cli",
"binary": "ruby",
"semver": ">=3.2.0",
"semver": ">=3.0.0",
"error": "visit rvm install https://rvm.io/rvm/install",
"platform": "darwin"
},
{
"rule": "cli",
"binary": "pod",
"semver": "1.16.1",
"semver": "1.15.2",
"platform": "darwin"
}
],

31
.vscode/launch.json vendored
View file

@ -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
View file

@ -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"
}

View file

@ -1,3 +0,0 @@
# 필수 준수 사항
- agent-ops/rules/global.md 파일을 읽고 이에 대해 꼭 준수해서 진행해야한다.
- global.md 내용을 모르면 추정하지 말고 먼저 해당 파일을 확인한다.

View file

@ -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 단계에서 불필요한 세부 구현까지 생성하지 않는다.

View file

@ -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

View file

@ -1,3 +0,0 @@
# 필수 준수 사항
- agent-ops/rules/global.md 파일을 읽고 이에 대해 꼭 준수해서 진행해야한다.
- global.md 내용을 모르면 추정하지 말고 먼저 해당 파일을 확인한다.

View file

@ -1,3 +0,0 @@
# 필수 준수 사항
- agent-ops/rules/global.md 파일을 읽고 이에 대해 꼭 준수해서 진행해야한다.
- global.md 내용을 모르면 추정하지 말고 먼저 해당 파일을 확인한다.

View file

@ -1,4 +1,3 @@
source "https://rubygems.org"
gem "cocoapods", "1.16.1"
gem "rexml", ">= 3.4.2"
gem "cocoapods", "1.15.2"

View file

@ -5,32 +5,29 @@ GEM
base64
nkf
rexml
activesupport (7.2.2.1)
activesupport (7.1.3.4)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
mutex_m
tzinfo (~> 2.0)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
atomos (0.1.3)
base64 (0.2.0)
benchmark (0.4.0)
bigdecimal (3.1.8)
claide (1.1.0)
cocoapods (1.16.1)
cocoapods (1.15.2)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.16.1)
cocoapods-core (= 1.15.2)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 2.1, < 3.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
@ -44,8 +41,8 @@ GEM
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 2.3.0, < 3.0)
xcodeproj (>= 1.26.0, < 2.0)
cocoapods-core (1.16.1)
xcodeproj (>= 1.23.0, < 2.0)
cocoapods-core (1.15.2)
activesupport (>= 5.0, < 8)
addressable (~> 2.8)
algoliasearch (~> 1.0)
@ -65,7 +62,7 @@ GEM
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored2 (3.1.2)
concurrent-ruby (1.3.4)
concurrent-ruby (1.3.3)
connection_pool (2.4.1)
drb (2.2.1)
escape (0.0.4)
@ -76,38 +73,38 @@ GEM
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
httpclient (2.8.3)
i18n (1.14.6)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
json (2.9.0)
logger (1.6.3)
minitest (5.25.4)
json (2.7.2)
minitest (5.23.1)
molinillo (0.8.0)
nanaimo (0.4.0)
mutex_m (0.2.0)
nanaimo (0.3.0)
nap (1.1.0)
netrc (0.11.0)
nkf (0.2.0)
public_suffix (4.0.7)
rexml (3.4.4)
rexml (3.2.9)
strscan
ruby-macho (2.5.1)
securerandom (0.4.0)
strscan (3.1.0)
typhoeus (1.4.1)
ethon (>= 0.9.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
xcodeproj (1.27.0)
xcodeproj (1.24.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
PLATFORMS
ruby
DEPENDENCIES
cocoapods (= 1.16.1)
rexml (>= 3.4.2)
cocoapods (= 1.15.2)
BUNDLED WITH
2.5.11

View file

@ -20,21 +20,6 @@ A spec-compliant polyfill for Intl.DateTimeFormat fully tested by the official E
* LICENSE: MIT
---
## @formatjs/intl-displaynames
This product contains '@formatjs/intl-displaynames' by Linjie Ding.
Polyfill for: https://tc39.es/proposal-intl-displaynames
* HOMEPAGE:
* https://github.com/formatjs/formatjs
* LICENSE: MIT
---
## @formatjs/intl-getcanonicallocales
@ -102,21 +87,6 @@ A spec-compliant polyfill for Intl.PluralRules fully tested by the official ECMA
* LICENSE: MIT
---
## @formatjs/intl-relativetimeformat
This product contains '@formatjs/intl-relativetimeformat' by Long Ho.
Formats JavaScript dates to relative time strings.
* HOMEPAGE:
* https://github.com/formatjs/formatjs
* LICENSE: MIT
---
## @gorhom/bottom-sheet
@ -150,42 +120,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## @gorhom/portal
This product contains '@gorhom/portal' by Mo Gorhom.
A simplified portal implementation for ⭕️ React Native ⭕️
* HOMEPAGE:
* https://github.com/gorhom/react-native-portal#readme
* LICENSE: MIT
MIT License
Copyright (c) 2020 Mo Gorhom
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## @mattermost/calls
@ -309,39 +243,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## @mattermost/secure-pdf-viewer
This product contains '@mattermost/secure-pdf-viewer' by Mattermost.
Securely view PDF documents without allowing them to be downloaded to their devices.
* HOMEPAGE:
* https://github.com/mattermost/secure-pdf-viewer
* LICENSE: MIT
MIT License
Copyright (c) 2022 Mattermost, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
@ -450,114 +351,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## @react-native-community/cli
This product contains '@react-native-community/cli'.
React Native CLI
* HOMEPAGE:
* https://github.com/react-native-community/cli/tree/main/packages/cli
* LICENSE: MIT
MIT License
Copyright (c) 2018 react-native-community
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## @react-native-community/cli-platform-android
This product contains '@react-native-community/cli-platform-android'.
This package is part of the [React Native CLI](../../README.md). It contains commands for managing the Android part of React Native app.
* HOMEPAGE:
* https://github.com/react-native-community/cli/tree/main/packages/cli-platform-android
* LICENSE: MIT
MIT License
Copyright (c) 2018 react-native-community
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## @react-native-community/cli-platform-ios
This product contains '@react-native-community/cli-platform-ios'.
This package is part of the [React Native CLI](../../README.md). It contains commands for managing iOS part of React Native app.
* HOMEPAGE:
* https://github.com/react-native-community/cli/tree/main/packages/cli-platform-ios
* LICENSE: MIT
MIT License
Copyright (c) 2018 react-native-community
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## @react-native-community/datetimepicker
@ -796,28 +589,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## @shopify/flash-list
This product contains '@shopify/flash-list' by shopify.
FlashList is a more performant FlatList replacement
* HOMEPAGE:
* https://shopify.github.io/flash-list/
* LICENSE: MIT
Copyright 2022-present, Shopify Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## @stream-io/flat-list-mvcp
@ -1502,41 +1273,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## fflate
This product contains 'fflate' by Arjun Barrett.
High performance (de)compression in an 8kB package
* HOMEPAGE:
* https://101arrowz.github.io/fflate
* LICENSE: MIT
MIT License
Copyright (c) 2023 Arjun Barrett
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## fuse.js
@ -1753,43 +1489,6 @@ Lightweight fuzzy-search
limitations under the License.
---
## grapheme-splitter
This product contains 'grapheme-splitter' by Orlin Georgiev.
A JavaScript library that breaks strings into their individual user-perceived characters. It supports emojis!
* HOMEPAGE:
* https://github.com/orling/grapheme-splitter
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2015 Orlin Georgiev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## html-entities
@ -1824,43 +1523,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
## js-sha256
This product contains 'js-sha256' by Chen, Yi-Cyuan.
A simple SHA-256 / SHA-224 hash function for JavaScript supports UTF-8 encoding.
* HOMEPAGE:
* https://github.com/emn178/js-sha256
* LICENSE: MIT
Copyright (c) 2014-2025 Chen, Yi-Cyuan
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## mime-db
@ -1934,24 +1596,39 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## node-html-parser
## pako
This product contains 'node-html-parser' by Xiaoyi Shi.
This product contains 'pako'.
A very fast HTML parser, generating a simplified DOM, with basic element query support.
zlib port to javascript - fast, modularized, with browser support
* HOMEPAGE:
* https://github.com/taoqf/node-fast-html-parser
* https://github.com/nodeca/pako#readme
* LICENSE: MIT
* LICENSE: (MIT AND Zlib)
Copyright 2019 Tao Qiufeng
(The MIT License)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Copyright (C) 2014-2017 by Vitaly Puzrin and Andrei Tuputcyn
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
@ -1988,42 +1665,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
## prismjs
This product contains 'prismjs' by Lea Verou.
Lightweight, robust, elegant syntax highlighting. A spin-off project from Dabblet.
* HOMEPAGE:
* https://github.com/PrismJS/prism#readme
* LICENSE: MIT
MIT LICENSE
Copyright (c) 2012 Lea Verou
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
## react
@ -2525,42 +2166,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-keyboard-controller
This product contains 'react-native-keyboard-controller' by Kiryl Ziusko.
Keyboard manager which works in identical way on both iOS and Android
* HOMEPAGE:
* https://kirillzyusko.github.io/react-native-keyboard-controller/
* LICENSE: MIT
MIT License
Copyright (c) 2021 Kiryl Ziusko
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-keychain
@ -3046,42 +2651,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-url-polyfill
This product contains 'react-native-url-polyfill' by Nicolas Charpentier.
A lightweight and trustworthy URL polyfill for React Native
* HOMEPAGE:
* https://github.com/charpeni/react-native-url-polyfill
* LICENSE: MIT
MIT License
Copyright (c) 2019 Nicolas Charpentier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-vector-icons
@ -3346,4 +2915,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,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 (9.5.0+)
- **Supported iOS versions:** 13.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

View file

@ -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 패턴을 따른다
## 금지 사항
- < 도메인에서 하면 되는 >

View file

@ -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 누락

View file

@ -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()` 처리)

View file

@ -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()` 호출

View file

@ -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()` 호출

View file

@ -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 컴포넌트 내부에 인라인 삽입

View file

@ -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 만 최소로 로드한다.

View file

@ -1,35 +0,0 @@
---
name: <skill-name>
description: <skill 설명>
version: 1.0.0
---
# <Skill Name>
## 목적
< skill이 해결하는 문제>
## 언제 호출할지
- < skill을 사용해야 하는 상황>
- < skill을 사용해야 하는 상황>
## 입력
- `<param>`: <설명> (필수/선택)
## 먼저 확인할 것
- [ ] <사전 확인 항목>
- [ ] <사전 확인 항목>
## 실행 절차
1. <단계>
2. <단계>
3. <단계>
## 출력 형식
```
<출력 예시>
```
## 금지 사항
- <하면 되는 >
- <하면 되는 >

View file

@ -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 파일 수정 금지)

View file

@ -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 파일 생성만 담당한다

View file

@ -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 에 여러 독립적인 책임을 묶지 않는다

View file

@ -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까지 로드하지 않는다

View file

@ -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로 먼저 판별한다.

View file

@ -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.
@ -104,7 +100,7 @@ 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 +108,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 544
versionName "2.19.0"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
@ -141,7 +137,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())
}
}
@ -190,7 +186,6 @@ repositories {
maven {
url 'https://maven.google.com'
}
maven { url 'https://jitpack.io' }
}
dependencies {
@ -203,23 +198,13 @@ dependencies {
implementation jscFlavor
}
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
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-jsi')
@ -232,16 +217,15 @@ dependencies {
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'
}
if (details.requested.name == 'play-services-tasks') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.2.0'
}
if (details.requested.name == 'play-services-tasks') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.0.2'
}
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.2.0'
}
if (details.requested.name == 'okhttp') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.12.0'
@ -264,3 +248,4 @@ tasks.register('copyDownloadableDepsToLibs', Copy) {
}
apply plugin: 'com.google.gms.google-services'
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)

View file

@ -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"
}
}

View file

@ -1,4 +1,4 @@
package com.tokilabs.mattermost;
package com.mattermost.rnbeta;
import com.wix.detox.Detox;
import com.wix.detox.config.DetoxConfig;

View file

@ -11,6 +11,8 @@
<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.READ_MEDIA_IMAGES" tools:ignore="SelectedPhotoAccess" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:ignore="SelectedPhotoAccess" />
<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.ACCESS_NETWORK_STATE" />
@ -77,7 +79,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"

View file

@ -1,4 +0,0 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View file

@ -20,6 +20,7 @@ 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,10 +29,9 @@ import androidx.core.app.Person;
import androidx.core.app.RemoteInput;
import androidx.core.graphics.drawable.IconCompat;
import com.tokilabs.mattermost.*;
import com.mattermost.rnbeta.*;
import com.mattermost.rnutils.helpers.NotificationHelper;
import com.nozbe.watermelondb.WMDatabase;
import com.mattermost.turbolog.TurboLog;
import java.io.IOException;
import java.security.KeyFactory;
@ -241,36 +241,36 @@ 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");
Log.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");
Log.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");
Log.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");
Log.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");
Log.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");
Log.i("Mattermost Notifications Signature verification", "Invalid server version");
return false;
}
@ -324,7 +324,7 @@ public class CustomPushNotificationHelper {
}
if (rejected) {
TurboLog.Companion.i("Mattermost Notifications Signature verification", "Server version should send signature");
Log.i("Mattermost Notifications Signature verification", "Server version should send signature");
return false;
}
@ -334,7 +334,7 @@ public class CustomPushNotificationHelper {
String signingKey = queryConfigSigningKey(db);
if (signingKey == null) {
TurboLog.Companion.i("Mattermost Notifications Signature verification", "No signing key");
Log.i("Mattermost Notifications Signature verification", "No signing key");
return false;
}
@ -345,17 +345,17 @@ public class CustomPushNotificationHelper {
String storedDeviceToken = getDeviceToken(dbHelper);
if (storedDeviceToken == null) {
TurboLog.Companion.i("Mattermost Notifications Signature verification", "No device token stored");
Log.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");
Log.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");
Log.i("Mattermost Notifications Signature verification", "Empty stored device token");
return false;
}
@ -366,19 +366,19 @@ public class CustomPushNotificationHelper {
.build()
.parseSignedClaims(signature);
} catch (MissingClaimException e) {
TurboLog.Companion.i("Mattermost Notifications Signature verification", String.format("Missing claim: %s", e.getMessage()));
Log.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()));
Log.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()));
Log.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()));
Log.i("Mattermost Notifications Signature verification", String.format("Exception while parsing JWT: %s", e.getMessage()));
e.printStackTrace();
return false;
}
@ -572,7 +572,7 @@ 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();
@ -594,7 +594,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);
}

View file

@ -10,7 +10,6 @@ import com.facebook.react.bridge.ReadableMap;
import com.mattermost.networkclient.ApiClientModuleImpl;
import com.mattermost.networkclient.enums.RetryTypes;
import com.mattermost.turbolog.TurboLog;
import okhttp3.HttpUrl;
import okhttp3.Response;
@ -22,12 +21,9 @@ public class Network {
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 = (ApiClientModuleImpl.context == null) ? new ReactApplicationContext(context) : ApiClientModuleImpl.context;
clientModule = new ApiClientModuleImpl(reactContext);
createClientOptions();
}
public static void get(String baseUrl, String endpoint, ReadableMap options, Promise promise) {

View file

@ -2,6 +2,7 @@ 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
@ -14,7 +15,6 @@ import com.mattermost.helpers.push_notification.fetchNeededUsers
import com.mattermost.helpers.push_notification.fetchPosts
import com.mattermost.helpers.push_notification.fetchTeamIfNeeded
import com.mattermost.helpers.push_notification.fetchThread
import com.mattermost.turbolog.TurboLog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -50,38 +50,31 @@ class PushNotificationDataRunnable {
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 and ack=$ackId")
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) }
}
@ -111,15 +104,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 and ack=$ackId")
}
} catch (e: Exception) {
e.printStackTrace()
val eMessage = e.message ?: "Error with no message"
TurboLog.e("ReactNative", "Error processing push notification error=$eMessage")
} finally {
db?.close()
TurboLog.i("ReactNative", "DONE fetching notification data")
Log.i("ReactNative", "DONE fetching notification data")
}
return result
@ -135,22 +126,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)
}
}
}

View file

@ -4,6 +4,10 @@ import java.util.UUID
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()
}

View file

@ -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) {
}
}

View file

@ -54,14 +54,12 @@ fun insertCategoriesWithChannels(db: WMDatabase, 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) }
}
}
}
@ -69,22 +67,19 @@ fun insertCategoriesWithChannels(db: WMDatabase, orderCategories: ReadableMap) {
fun insertChannelToDefaultCategory(db: WMDatabase, 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()

View file

@ -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)
}

View file

@ -10,10 +10,8 @@ import com.nozbe.watermelondb.WMDatabase
internal fun findPostInChannel(chunks: ReadableArray, earliest: Double, latest: Double): ReadableMap? {
for (i in 0 until chunks.size()) {
val chunk = chunks.getMap(i)
chunk?.let {
if (earliest >= it.getDouble("earliest") || latest <= it.getDouble("latest")) {
return it
}
if (earliest >= chunk.getDouble("earliest") || latest <= chunk.getDouble("latest")) {
return chunk
}
}
@ -47,12 +45,10 @@ internal fun mergePostsInChannel(db: WMDatabase, existingChunks: ReadableArray,
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()

View file

@ -1,6 +1,5 @@
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
@ -10,21 +9,6 @@ import com.mattermost.helpers.mapCursor
import com.nozbe.watermelondb.WMDatabase
import org.json.JSONObject
private fun getLastReplyAt(thread: ReadableMap): Double {
try {
var v = thread.getDouble("last_reply_at")
if (v == 0.0) {
val post = thread.getMap("post")
if (post != null) {
v = post.getDouble("create_at")
}
}
return v
} catch (e: NoSuchKeyException) {
return 0.0
}
}
internal fun insertThread(db: WMDatabase, thread: ReadableMap) {
// These fields are not present when we extract threads from posts
try {
@ -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(
@ -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(
@ -83,17 +67,15 @@ internal fun insertThreadParticipants(db: WMDatabase, threadId: String, particip
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()
}
@ -249,19 +231,8 @@ fun handleThreadInTeam(db: WMDatabase, thread: ReadableMap, teamId: String) {
fun handleTeamThreadsSync(db: WMDatabase, threadList: ArrayList<ReadableMap>, teamIds: ArrayList<String>) {
val sortedList = threadList.filter{ it.getBoolean("is_following") }
.sortedBy {
var v = getLastReplyAt(it)
if (v == 0.0) {
Log.d("Database", "Trying to add a thread with no replies and no post")
}
v
}
.map {
getLastReplyAt(it)
}
if (sortedList.isEmpty()) {
return;
}
.sortedBy { it.getDouble("last_reply_at") }
.map { it.getDouble("last_reply_at") }
val earliest = sortedList.first()
val latest = sortedList.last()

View file

@ -48,40 +48,38 @@ fun getCurrentUserLocale(db: WMDatabase): String {
fun handleUsers(db: WMDatabase, 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()
}
}
}

View file

@ -26,13 +26,11 @@ suspend fun PushNotificationDataRunnable.Companion.fetchMyChannel(db: WMDatabase
"D" -> {
profilesArray = fetchProfileInChannel(db, serverUrl, channelId)
if ((profilesArray?.size() ?: 0) > 0) {
profilesArray?.getMap(0)?.let { profile ->
val displayName = displayUsername(profile, displayNameSetting)
val data = Arguments.createMap()
data.merge(channelData)
data.putString("display_name", displayName)
channelData = data
}
val displayName = displayUsername(profilesArray!!.getMap(0), displayNameSetting)
val data = Arguments.createMap()
data.merge(channelData)
data.putString("display_name", displayName)
channelData = data
}
}
"G" -> {
@ -119,10 +117,8 @@ private suspend fun PushNotificationDataRunnable.Companion.fetchProfileInChannel
if (profilesArray != null) {
for (i in 0 until profilesArray.size()) {
val profile = profilesArray.getMap(i)
profile?.let {
if (it.getString("id") != currentUserId) {
result.pushMap(it)
}
if (profile.getString("id") != currentUserId) {
result.pushMap(profile)
}
}
}
@ -156,9 +152,7 @@ private fun PushNotificationDataRunnable.Companion.displayGroupMessageName(profi
val names = ArrayList<String>()
for (i in 0 until profilesArray.size()) {
val profile = profilesArray.getMap(i)
profile?.let {
names.add(displayUsername(it, displayNameSetting))
}
names.add(displayUsername(profile, displayNameSetting))
}
return names.sortedWith { s1, s2 ->

View file

@ -22,12 +22,12 @@ internal suspend fun PushNotificationDataRunnable.Companion.fetch(serverUrl: Str
}
}
override fun reject(code: String, message: String?) {
override fun reject(code: String, message: String) {
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
}
override fun reject(throwable: Throwable) {
cont.resumeWith(Result.failure(IOException("Unexpected code $throwable")))
override fun reject(reason: Throwable?) {
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
}
})
}
@ -41,12 +41,12 @@ internal suspend fun PushNotificationDataRunnable.Companion.fetchWithPost(server
cont.resumeWith(Result.success(response))
}
override fun reject(code: String, message: String?) {
override fun reject(code: String, message: String) {
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
}
override fun reject(throwable: Throwable) {
cont.resumeWith(Result.failure(IOException("Unexpected code $throwable")))
override fun reject(reason: Throwable?) {
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
}
})
}

View file

@ -62,7 +62,7 @@ internal suspend fun PushNotificationDataRunnable.Companion.fetchPosts(
val userIdsAlreadyLoaded = mutableListOf<String>()
if (loadedProfiles != null) {
for (i in 0 until loadedProfiles.size()) {
loadedProfiles.getMap(i)?.getString("id")?.let { userIdsAlreadyLoaded.add(it) }
loadedProfiles.getMap(i).getString("id")?.let { userIdsAlreadyLoaded.add(it) }
}
}
@ -95,12 +95,10 @@ internal suspend fun PushNotificationDataRunnable.Companion.fetchPosts(
if (attachments != null) {
for (i in 0 until attachments.size()) {
val attachment = attachments.getMap(i)
attachment?.let {
val pretext = it.getString("pretext")
val text = it.getString("text")
findNeededUsernames(pretext)
findNeededUsernames(text)
}
val pretext = attachment.getString("pretext")
val text = attachment.getString("text")
findNeededUsernames(pretext)
findNeededUsernames(text)
}
}
@ -134,23 +132,22 @@ internal suspend fun PushNotificationDataRunnable.Companion.fetchPosts(
participants?.let {
for (i in 0 until it.size()) {
val participant = it.getMap(i)
participant?.let { p ->
val participantId = p.getString("id")
if (participantId != currentUserId && participantId != null) {
if (!threadParticipantUserIds.contains(participantId) && !userIdsAlreadyLoaded.contains(participantId)) {
threadParticipantUserIds.add(participantId)
}
if (!threadParticipantUsers.containsKey(participantId)) {
threadParticipantUsers[participantId] = p
}
val participantId = participant.getString("id")
if (participantId != currentUserId && participantId != null) {
if (!threadParticipantUserIds.contains(participantId) && !userIdsAlreadyLoaded.contains(participantId)) {
threadParticipantUserIds.add(participantId)
}
val username = p.getString("username")
if (username != null && username != currentUsername && !threadParticipantUsernames.contains(username)) {
threadParticipantUsernames.add(username)
if (!threadParticipantUsers.containsKey(participantId)) {
threadParticipantUsers[participantId] = participant
}
}
val username = participant.getString("username")
if (username != null && username != currentUsername && !threadParticipantUsernames.contains(username)) {
threadParticipantUsernames.add(username)
}
}
}
}

View file

@ -56,6 +56,6 @@ internal suspend fun PushNotificationDataRunnable.Companion.fetchNeededUsers(ser
internal fun PushNotificationDataRunnable.Companion.addUsersToList(users: ReadableArray, list: ArrayList<Any>) {
for (i in 0 until users.size()) {
users.getMap(i)?.let { list.add(it) }
list.add(users.getMap(i))
}
}

View file

@ -1,8 +1,9 @@
package com.tokilabs.mattermost
package com.mattermost.rnbeta
import android.app.PendingIntent
import android.content.Context
import android.os.Bundle
import android.util.Log
import androidx.core.app.NotificationCompat
import com.mattermost.helpers.CustomPushNotificationHelper
import com.mattermost.helpers.DatabaseHelper
@ -10,7 +11,6 @@ import com.mattermost.helpers.Network
import com.mattermost.helpers.PushNotificationDataHelper
import com.mattermost.helpers.database_extension.getServerUrlForIdentifier
import com.mattermost.rnutils.helpers.NotificationHelper
import com.mattermost.turbolog.TurboLog
import com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME
import com.wix.reactnativenotifications.core.AppLaunchHelper
import com.wix.reactnativenotifications.core.AppLifecycleFacade
@ -33,6 +33,7 @@ class CustomPushNotification(
init {
try {
DatabaseHelper.instance?.init(context)
Network.init(context)
NotificationHelper.cleanNotificationPreferencesIfNeeded(context)
} catch (e: Exception) {
e.printStackTrace()
@ -50,7 +51,6 @@ class CustomPushNotification(
val isIdLoaded = initialData.getString("id_loaded") == "true"
val notificationId = NotificationHelper.getNotificationId(initialData)
val serverUrl = addServerUrlToBundle(initialData)
Network.init(mContext)
GlobalScope.launch {
try {
@ -84,7 +84,7 @@ class CustomPushNotification(
}
if (!CustomPushNotificationHelper.verifySignature(mContext, signature, serverUrl, ackId)) {
TurboLog.i("Mattermost Notifications Signature verification", "Notification skipped because we could not verify it.")
Log.i("Mattermost Notifications Signature verification", "Notification skipped because we could not verify it.")
return
}
@ -104,8 +104,8 @@ class CustomPushNotification(
when (type) {
CustomPushNotificationHelper.PUSH_TYPE_MESSAGE, CustomPushNotificationHelper.PUSH_TYPE_SESSION -> {
val currentActivityName = mAppLifecycleFacade.runningReactContext?.currentActivity?.componentName?.className ?: ""
TurboLog.i("ReactNative", currentActivityName)
if (!mAppLifecycleFacade.isAppVisible() || !currentActivityName.contains("MainActivity")) {
Log.i("ReactNative", currentActivityName)
if (!mAppLifecycleFacade.isAppVisible() || currentActivityName != "MainActivity") {
var createSummary = type == CustomPushNotificationHelper.PUSH_TYPE_MESSAGE
if (type == CustomPushNotificationHelper.PUSH_TYPE_MESSAGE) {
channelId?.let {

View file

@ -1,9 +1,8 @@
package com.tokilabs.mattermost
package com.mattermost.rnbeta
import android.content.res.Configuration
import android.os.Bundle
import android.view.KeyEvent
import androidx.core.view.WindowCompat
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
import com.facebook.react.defaults.DefaultReactActivityDelegate
@ -15,7 +14,6 @@ import expo.modules.ReactActivityDelegateWrapper
class MainActivity : NavigationActivity() {
private var HWKeyboardConnected = false
private val foldableObserver = FoldableObserver.getInstance(this)
private var lastOrientation: Int = Configuration.ORIENTATION_UNDEFINED
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
@ -36,9 +34,7 @@ class MainActivity : NavigationActivity() {
super.onCreate(null)
setContentView(R.layout.launch_screen)
setHWKeyboardConnected()
lastOrientation = this.resources.configuration.orientation
foldableObserver.onCreate()
WindowCompat.setDecorFitsSystemWindows(window, false)
}
override fun onStart() {
@ -58,11 +54,6 @@ class MainActivity : NavigationActivity() {
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val newOrientation = newConfig.orientation
if (newOrientation != lastOrientation) {
lastOrientation = newOrientation
foldableObserver.handleWindowLayoutInfo()
}
if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) {
HWKeyboardConnected = true
} else if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) {

View file

@ -1,27 +1,26 @@
package com.tokilabs.mattermost
package com.mattermost.rnbeta
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.util.Log
import com.facebook.react.PackageList
import com.facebook.react.ReactHost
import com.facebook.react.ReactInstanceManager
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.react.modules.network.OkHttpClientProvider
import com.facebook.soloader.SoLoader
import com.mattermost.networkclient.RCTOkHttpClientFactory
import com.mattermost.rnshare.helpers.RealPathUtil
import com.mattermost.turbolog.TurboLog
import com.mattermost.turbolog.ConfigureOptions
import com.nozbe.watermelondb.jsi.JSIInstaller
import com.nozbe.watermelondb.jsi.WatermelonDBJSIPackage
import com.reactnativenavigation.NavigationApplication
import com.wix.reactnativenotifications.RNNotificationsPackage
import com.wix.reactnativenotifications.core.AppLaunchHelper
@ -45,7 +44,6 @@ class MainApplication : NavigationApplication(), INotificationsApplication {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(RNNotificationsPackage(this@MainApplication))
add(WatermelonDBJSIPackage())
}
override fun getJSMainModuleName(): String = "index"
@ -65,9 +63,7 @@ class MainApplication : NavigationApplication(), INotificationsApplication {
// Delete any previous temp files created by the app
val tempFolder = File(applicationContext.cacheDir, RealPathUtil.CACHE_DIR_NAME)
RealPathUtil.deleteTempFiles(tempFolder)
TurboLog.configure(options = ConfigureOptions(logsDirectory = applicationContext.cacheDir.absolutePath + "/logs", logPrefix = applicationContext.packageName))
TurboLog.i("ReactNative", "Cleaning temp cache " + tempFolder.absolutePath)
Log.i("ReactNative", "Cleaning temp cache " + tempFolder.absolutePath)
// Tells React Native to use our RCTOkHttpClientFactory which builds an OKHttpClient
// with a cookie jar defined in APIClientModule and an interceptor to intercept all
@ -79,12 +75,13 @@ class MainApplication : NavigationApplication(), INotificationsApplication {
OkHttpClientProvider.setOkHttpClientFactory(RCTOkHttpClientFactory())
ExpoImageOkHttpClientGlideModule.okHttpClient = RCTOkHttpClientFactory().createNewNetworkModuleClient()
SoLoader.init(this, OpenSourceMergedSoMapping)
SoLoader.init(this, false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load(bridgelessEnabled = false)
}
ApplicationLifecycleDispatcher.onApplicationCreate(this)
registerJSIModules()
}
override fun onConfigurationChanged(newConfig: Configuration) {
@ -117,4 +114,27 @@ class MainApplication : NavigationApplication(), INotificationsApplication {
}
}
}
@Suppress("DEPRECATION")
private fun registerJSIModules() {
val reactInstanceManager = reactNativeHost.reactInstanceManager
if (!listenerAdded) {
listenerAdded = true
reactInstanceManager.addReactInstanceEventListener(object : ReactInstanceManager.ReactInstanceEventListener {
override fun onReactContextInitialized(context: ReactContext) {
runOnJSQueueThread {
registerWatermelonJSI(context)
}
}
})
}
}
private fun registerWatermelonJSI(context: ReactContext) {
val holder = context.javaScriptContextHolder?.get()
if (holder != null) {
JSIInstaller.install(context, holder)
}
}
}

View file

@ -1,12 +1,12 @@
package com.tokilabs.mattermost;
package com.mattermost.rnbeta;
import android.content.Context;
import android.content.Intent;
import android.app.IntentService;
import android.os.Bundle;
import android.util.Log;
import com.mattermost.rnutils.helpers.NotificationHelper;
import com.mattermost.turbolog.TurboLog;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationDismissService extends IntentService {
@ -20,6 +20,6 @@ public class NotificationDismissService extends IntentService {
final Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
NotificationHelper.INSTANCE.dismissNotification(context, bundle);
TurboLog.Companion.i("ReactNative", "Dismiss notification");
Log.i("ReactNative", "Dismiss notification");
}
}

View file

@ -1,4 +1,4 @@
package com.tokilabs.mattermost;
package com.mattermost.rnbeta;
import android.app.Notification;
import android.app.NotificationManager;
@ -8,18 +8,16 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.Person;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.mattermost.helpers.*;
import com.mattermost.turbolog.TurboLog;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
import com.wix.reactnativenotifications.core.notification.PushNotificationProps;
@ -42,7 +40,6 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
final int notificationId = intent.getIntExtra(CustomPushNotificationHelper.NOTIFICATION_ID, -1);
final String serverUrl = bundle.getString("server_url");
Network.init(context);
if (serverUrl != null) {
replyToMessage(serverUrl, notificationId, message);
} else {
@ -69,6 +66,7 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
WritableMap headers = Arguments.createMap();
headers.putString("Content-Type", "application/json");
WritableMap body = Arguments.createMap();
body.putString("channel_id", channelId);
body.putString("message", message.toString());
@ -80,36 +78,26 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
String postsEndpoint = "/api/v4/posts?set_online=false";
Network.post(serverUrl, postsEndpoint, options, new ResolvePromise() {
private boolean isSuccessful(int statusCode) {
return statusCode >= 200 && statusCode < 300;
}
@Override
public void resolve(@Nullable Object value) {
if (value != null) {
ReadableMap response = (ReadableMap)value;
ReadableMap data = response.getMap("data");
if (data != null && data.hasKey("status_code") && !isSuccessful(data.getInt("status_code"))) {
TurboLog.Companion.i("ReactNative", String.format("Reply FAILED exception %s", data.getString("message")));
onReplyFailed(notificationId);
return;
}
onReplySuccess(notificationId, message);
TurboLog.Companion.i("ReactNative", "Reply SUCCESS");
Log.i("ReactNative", "Reply SUCCESS");
} else {
TurboLog.Companion.i("ReactNative", "Reply FAILED resolved without value");
Log.i("ReactNative", "Reply FAILED resolved without value");
onReplyFailed(notificationId);
}
}
@Override
public void reject(@NonNull Throwable reason) {
TurboLog.Companion.i("ReactNative", String.format("Reply FAILED exception %s", reason.getMessage()));
public void reject(Throwable reason) {
Log.i("ReactNative", String.format("Reply FAILED exception %s", reason.getMessage()));
onReplyFailed(notificationId);
}
@Override
public void reject(@NonNull String code, String message) {
TurboLog.Companion.i("ReactNative",
public void reject(String code, String message) {
Log.i("ReactNative",
String.format("Reply FAILED status %s BODY %s", code, message)
);
onReplyFailed(notificationId);

View file

@ -1,6 +1,7 @@
package com.tokilabs.mattermost;
package com.mattermost.rnbeta;
import android.os.Bundle;
import android.util.Log;
import java.lang.System;
import java.util.Objects;
@ -11,7 +12,6 @@ import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.mattermost.helpers.*;
import com.mattermost.turbolog.TurboLog;
import okhttp3.Response;
@ -19,7 +19,7 @@ public class ReceiptDelivery {
private static final String[] ackKeys = new String[]{"post_id", "root_id", "category", "message", "team_id", "channel_id", "channel_name", "type", "sender_id", "sender_name", "version"};
public static Bundle send(final String ackId, final String serverUrl, final String postId, final String type, final boolean isIdLoaded) {
TurboLog.Companion.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with ID-LOADED=%s", ackId, type, serverUrl, isIdLoaded));
Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with ID-LOADED=%s", ackId, type, serverUrl, isIdLoaded));
WritableMap options = Arguments.createMap();
WritableMap headers = Arguments.createMap();
WritableMap body = Arguments.createMap();
@ -38,7 +38,6 @@ public class ReceiptDelivery {
JSONObject jsonResponse = new JSONObject(responseBody);
return parseAckResponse(jsonResponse);
} catch (Exception e) {
TurboLog.Companion.e("ReactNative", "Send receipt delivery failed " + e.getMessage());
e.printStackTrace();
return null;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 787 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 730 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 730 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 730 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

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