Compare commits
12 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d066425e6d | ||
|
|
105b5e0568 | ||
|
|
87b6c31689 | ||
|
|
bb6cfab2ef | ||
|
|
e844023bcf | ||
|
|
55d457785a | ||
|
|
87e9774bb5 | ||
|
|
34c6df6203 | ||
|
|
23f3d11524 | ||
|
|
d7aa424f37 | ||
|
|
970e9fe3c7 | ||
|
|
5cd2363b3b |
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -45,14 +45,15 @@ runs:
|
|||
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 }}
|
||||
key: ${{ runner.os }}-pods-v3-intune-${{ inputs.intune-enabled }}-${{ steps.intune-hash.outputs.hash }}-${{ hashFiles('ios/Podfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pods-v4-intune-${{ inputs.intune-enabled }}-${{ steps.intune-hash.outputs.hash }}-${{ hashFiles('ios/Podfile.lock') }}-
|
||||
${{ runner.os }}-pods-v3-intune-${{ inputs.intune-enabled }}-${{ steps.intune-hash.outputs.hash }}-
|
||||
${{ runner.os }}-pods-v3-intune-${{ inputs.intune-enabled }}-
|
||||
|
||||
- name: ci/install-pods-dependencies
|
||||
shell: bash
|
||||
env:
|
||||
INTUNE_ENABLED: ${{ inputs.intune-enabled }}
|
||||
INTUNE_ENABLED: ${{ inputs.intune-enabled == 'true' && '1' || '0' }}
|
||||
run: |
|
||||
echo "::group::install-pods-dependencies"
|
||||
echo "INTUNE_ENABLED=$INTUNE_ENABLED"
|
||||
|
|
|
|||
21
.github/workflows/build-android-beta.yml
vendored
|
|
@ -24,9 +24,6 @@ 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
|
||||
|
|
@ -61,21 +58,3 @@ jobs:
|
|||
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
|
||||
|
|
|
|||
19
.github/workflows/build-android-release.yml
vendored
|
|
@ -24,9 +24,6 @@ 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
|
||||
|
|
@ -61,19 +58,3 @@ jobs:
|
|||
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
|
||||
|
|
|
|||
45
.github/workflows/build-ios-beta.yml
vendored
|
|
@ -11,7 +11,7 @@ on:
|
|||
env:
|
||||
NODE_VERSION: 22.14.0
|
||||
TERM: xterm
|
||||
INTUNE_ENABLED: 'true'
|
||||
INTUNE_ENABLED: 1
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
|
@ -27,9 +27,6 @@ jobs:
|
|||
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
|
||||
|
|
@ -37,7 +34,7 @@ jobs:
|
|||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-enabled: 'true'
|
||||
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
||||
|
||||
- name: ci/build-ios-simulator
|
||||
|
|
@ -56,30 +53,11 @@ jobs:
|
|||
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
|
||||
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
|
||||
|
|
@ -92,7 +70,7 @@ jobs:
|
|||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-enabled: 'true'
|
||||
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
||||
|
||||
- name: ci/build-and-deploy-ios-beta
|
||||
|
|
@ -122,20 +100,3 @@ jobs:
|
|||
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
|
||||
|
|
|
|||
45
.github/workflows/build-ios-release.yml
vendored
|
|
@ -11,7 +11,7 @@ on:
|
|||
env:
|
||||
NODE_VERSION: 22.14.0
|
||||
TERM: xterm
|
||||
INTUNE_ENABLED: 'true'
|
||||
INTUNE_ENABLED: 1
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
|
@ -27,9 +27,6 @@ jobs:
|
|||
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
|
||||
|
|
@ -42,14 +39,13 @@ jobs:
|
|||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-enabled: 'true'
|
||||
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
||||
|
||||
- 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 }}"
|
||||
|
|
@ -74,31 +70,11 @@ jobs:
|
|||
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
|
||||
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
|
||||
|
|
@ -106,7 +82,7 @@ jobs:
|
|||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-enabled: 'true'
|
||||
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
||||
|
||||
- name: ci/build-ios-simulator
|
||||
|
|
@ -124,18 +100,3 @@ jobs:
|
|||
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
|
||||
|
|
|
|||
4
.github/workflows/build-pr.yml
vendored
|
|
@ -8,7 +8,7 @@ on:
|
|||
env:
|
||||
NODE_VERSION: 22.14.0
|
||||
TERM: xterm
|
||||
INTUNE_ENABLED: 'true'
|
||||
INTUNE_ENABLED: 1
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
|
@ -41,7 +41,7 @@ jobs:
|
|||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-enabled: 'true'
|
||||
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
||||
|
||||
- name: ci/build-ios-pr
|
||||
|
|
|
|||
2
.github/workflows/e2e-android-template.yml
vendored
|
|
@ -127,7 +127,7 @@ jobs:
|
|||
device_os_version: ${{ env.SDK_VERSION }}
|
||||
|
||||
e2e-android:
|
||||
name: machine-${{ matrix.runId }}-api-${{ matrix.deviceOsVersion }}
|
||||
name: android-detox-e2e-${{ matrix.runId }}-${{ matrix.deviceName }}-${{ matrix.deviceOsVersion }}
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
continue-on-error: true
|
||||
timeout-minutes: 240
|
||||
|
|
|
|||
23
.github/workflows/e2e-detox-pr.yml
vendored
|
|
@ -1,11 +1,12 @@
|
|||
# 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:
|
||||
branches:
|
||||
- main
|
||||
- feature_schedule_posts
|
||||
types:
|
||||
- labeled
|
||||
|
||||
|
|
@ -14,7 +15,7 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
INTUNE_ENABLED: 'true'
|
||||
INTUNE_ENABLED: 1
|
||||
|
||||
jobs:
|
||||
update-initial-status-ios:
|
||||
|
|
@ -48,7 +49,7 @@ jobs:
|
|||
|
||||
build-ios-simulator:
|
||||
if: contains(github.event.label.name, 'E2E iOS tests for PR')
|
||||
runs-on: macos-26
|
||||
runs-on: macos-15
|
||||
needs:
|
||||
- update-initial-status-ios
|
||||
steps:
|
||||
|
|
@ -58,25 +59,23 @@ jobs:
|
|||
- name: Prepare iOS Build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-enabled: 'true'
|
||||
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
|
||||
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
|
||||
|
|
@ -123,7 +122,7 @@ jobs:
|
|||
|
||||
run-ios-tests-on-pr:
|
||||
if: contains(github.event.label.name, 'E2E iOS tests for PR')
|
||||
name: iOS
|
||||
name: iOS Mobile Tests on PR
|
||||
uses: ./.github/workflows/e2e-ios-template.yml
|
||||
needs:
|
||||
- build-ios-simulator
|
||||
|
|
@ -135,7 +134,7 @@ jobs:
|
|||
|
||||
run-android-tests-on-pr:
|
||||
if: contains(github.event.label.name, 'E2E Android tests for PR')
|
||||
name: Android
|
||||
name: Android Mobile Tests on PR
|
||||
uses: ./.github/workflows/e2e-android-template.yml
|
||||
needs:
|
||||
- build-android-apk
|
||||
|
|
|
|||
4
.github/workflows/e2e-detox-release.yml
vendored
|
|
@ -6,7 +6,7 @@ on:
|
|||
- release-*
|
||||
|
||||
env:
|
||||
INTUNE_ENABLED: 'true'
|
||||
INTUNE_ENABLED: 1
|
||||
|
||||
jobs:
|
||||
update-initial-status-ios:
|
||||
|
|
@ -46,7 +46,7 @@ jobs:
|
|||
- name: Prepare iOS Build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-enabled: 'true'
|
||||
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
||||
|
||||
- name: Build iOS Simulator
|
||||
|
|
|
|||
4
.github/workflows/e2e-detox-scheduled.yml
vendored
|
|
@ -5,7 +5,7 @@ on:
|
|||
- cron: "0 0 * * 4,5" # Wednesday and Thursday midnight
|
||||
|
||||
env:
|
||||
INTUNE_ENABLED: 'true'
|
||||
INTUNE_ENABLED: 1
|
||||
|
||||
jobs:
|
||||
update-initial-status-ios:
|
||||
|
|
@ -45,7 +45,7 @@ jobs:
|
|||
- name: Prepare iOS Build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-enabled: 'true'
|
||||
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
||||
|
||||
- name: Build iOS Simulator
|
||||
|
|
|
|||
178
.github/workflows/e2e-ios-template.yml
vendored
|
|
@ -38,12 +38,12 @@ on:
|
|||
description: "iPhone simulator name"
|
||||
required: false
|
||||
type: string
|
||||
default: "iPhone 17 Pro"
|
||||
default: "iPhone 15 Pro"
|
||||
ios_device_os_name:
|
||||
description: "iPhone simulator OS version"
|
||||
required: false
|
||||
type: string
|
||||
default: "iOS 26.2"
|
||||
default: "iOS 17.2"
|
||||
low_bandwidth_mode:
|
||||
description: "Enable low bandwidth mode"
|
||||
required: false
|
||||
|
|
@ -121,7 +121,7 @@ jobs:
|
|||
device_os_version: ${{ env.DEVICE_OS_VERSION }}
|
||||
|
||||
e2e-ios:
|
||||
name: machine-${{ matrix.runId }}-os-${{ matrix.deviceOsVersion }}
|
||||
name: ios-detox-e2e-${{ matrix.runId }}-${{ matrix.deviceName }}-${{ matrix.deviceOsVersion }}
|
||||
runs-on: macos-15
|
||||
continue-on-error: true
|
||||
timeout-minutes: ${{ inputs.low_bandwidth_mode && 240 || 180 }}
|
||||
|
|
@ -173,82 +173,6 @@ jobs:
|
|||
- 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
|
||||
|
|
@ -258,111 +182,35 @@ jobs:
|
|||
|
||||
- name: Set .env with RUNNING_E2E=true
|
||||
run: |
|
||||
echo "RUNNING_E2E=true" > .env
|
||||
cat > .env <<EOF
|
||||
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
|
||||
- name: 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
|
||||
|
||||
# Start the server
|
||||
npm run start &
|
||||
sleep 120 # Wait for watchman to finish querying the files
|
||||
cd detox
|
||||
npm run clean-detox
|
||||
npm run detox:config-gen
|
||||
npm run e2e:ios-test -- ${{ matrix.specs }}
|
||||
env:
|
||||
DETOX_DISABLE_HIERARCHY_DUMP: "YES"
|
||||
DETOX_DISABLE_SCREENSHOT_TRACKING: "YES"
|
||||
DETOX_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
|
||||
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;
|
||||
|
||||
|
|
|
|||
4
.github/workflows/github-release.yml
vendored
|
|
@ -15,7 +15,7 @@ env:
|
|||
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
|
||||
SNYK_VERSION: "1.1297.2"
|
||||
CYCLONEDX_VERSION: "v0.27.2"
|
||||
INTUNE_ENABLED: 'true'
|
||||
INTUNE_ENABLED: 1
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
with:
|
||||
intune-enabled: ${{ env.INTUNE_ENABLED }}
|
||||
intune-enabled: 'true'
|
||||
intune-ssh-private-key: ${{ secrets.MM_MOBILE_INTUNE_DEPLOY_KEY }}
|
||||
|
||||
- name: ci/build-ios-unsigned
|
||||
|
|
|
|||
310
.gitignore
vendored
|
|
@ -1,199 +1,135 @@
|
|||
# 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
|
||||
env.d.ts
|
||||
*.apk
|
||||
*.aab
|
||||
*.ipa
|
||||
.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
|
||||
|
||||
*/**/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/*
|
||||
.aider*
|
||||
|
||||
# Intune submodule (private repository - internal builds only)
|
||||
# Track submodule reference but ignore content
|
||||
libraries/@mattermost/intune/*
|
||||
!libraries/@mattermost/intune/.gitkeep
|
||||
|
||||
# Prevent committing Intune module in node_modules
|
||||
node_modules/@mattermost/intune
|
||||
# Claude Code
|
||||
.claude/settings.local.json
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# 필수 준수 사항
|
||||
- agent-ops/rules/global.md 파일을 읽고 이에 대해 꼭 준수해서 진행해야한다.
|
||||
- global.md 내용을 모르면 추정하지 말고 먼저 해당 파일을 확인한다.
|
||||
|
|
@ -1 +0,0 @@
|
|||
-
|
||||
|
|
@ -1 +0,0 @@
|
|||
-
|
||||
|
|
@ -1 +0,0 @@
|
|||
-
|
||||
|
|
@ -1 +0,0 @@
|
|||
-
|
||||
|
|
@ -1 +0,0 @@
|
|||
-
|
||||
|
|
@ -1 +0,0 @@
|
|||
-
|
||||
31
.vscode/launch.json
vendored
|
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Metro: Start",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "bash",
|
||||
"runtimeArgs": [
|
||||
"-c",
|
||||
"lsof -ti:8081 | xargs kill -9 2>/dev/null; npm start"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
},
|
||||
{
|
||||
"name": "Android: Run App",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeExecutable": "bash",
|
||||
"runtimeArgs": [
|
||||
"-c",
|
||||
"adb devices | grep -q emulator || (emulator -avd Pixel_7_Pro &); adb -e wait-for-device && npm run android -- --deviceId emulator-5554"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
.vscode/settings.json
vendored
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"react-native.packager.port": 8081,
|
||||
"terminal.integrated.env.osx": {
|
||||
"NVM_DIR": "${env:HOME}/.nvm",
|
||||
"JAVA_HOME": "/opt/homebrew/opt/openjdk@17",
|
||||
"ANDROID_HOME": "${env:HOME}/Library/Android/sdk",
|
||||
"ANDROID_SDK_ROOT": "${env:HOME}/Library/Android/sdk"
|
||||
},
|
||||
"terminal.integrated.profiles.osx": {
|
||||
"zsh": {
|
||||
"path": "/bin/zsh",
|
||||
"args": ["-l"]
|
||||
}
|
||||
},
|
||||
"terminal.integrated.defaultProfile.osx": "zsh",
|
||||
"npm.scriptExplorerAction": "run"
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# 필수 준수 사항
|
||||
- agent-ops/rules/global.md 파일을 읽고 이에 대해 꼭 준수해서 진행해야한다.
|
||||
- global.md 내용을 모르면 추정하지 말고 먼저 해당 파일을 확인한다.
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
---
|
||||
name: scaffold-agent-ops
|
||||
description: 프로젝트 상태를 판별하고 에이전트 진입 파일과 agent-ops 기본 스캐폴드를 생성하기 위한 초기 규칙
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# 목적
|
||||
|
||||
이 스킬은 프로젝트에 Agent-Ops 구조가 없거나 불완전할 때,
|
||||
현재 프로젝트 상태를 분석하여 다음을 세팅한다.
|
||||
|
||||
1. 에이전트 진입 파일
|
||||
2. agent-ops 기본 폴더 구조
|
||||
3. global rule
|
||||
4. domain rule 템플릿
|
||||
5. skill router
|
||||
6. skill 템플릿
|
||||
7. 초기 domain / skill 세팅
|
||||
|
||||
이 스킬은 과도한 초기 생성보다
|
||||
"현재 프로젝트에 맞는 최소 스캐폴드" 생성을 우선한다.
|
||||
|
||||
# 핵심 원칙
|
||||
|
||||
- 기존 구조를 우선한다.
|
||||
- 새 파일 생성은 꼭 필요한 최소 범위로 제한한다.
|
||||
- 에이전트별 md는 얇은 진입점으로 둔다.
|
||||
- 실제 규칙은 agent-ops 아래로 모은다.
|
||||
- 도메인은 발명하지 말고 현재 구조에서 발견한다.
|
||||
- 처음에는 핵심 도메인만 생성한다.
|
||||
- 반복되는 작업만 초기 skill로 만든다.
|
||||
- 불확실한 내용은 후보로 제시한다.
|
||||
|
||||
# 상태 판별
|
||||
|
||||
## 신규 프로젝트
|
||||
다음 조건이 많으면 신규 프로젝트로 본다.
|
||||
|
||||
- 코드/폴더 구조가 단순하다
|
||||
- 도메인 경계가 아직 약하다
|
||||
- Agent-Ops 관련 파일이 없다
|
||||
- 반복 작업 패턴이 아직 적다
|
||||
|
||||
## 운영중 프로젝트
|
||||
다음 조건이 많으면 운영중 프로젝트로 본다.
|
||||
|
||||
- 모듈/폴더/패키지 경계가 보인다
|
||||
- 반복 작업이 드러난다
|
||||
- 기존 규칙/문서/관례의 흔적이 있다
|
||||
- 핵심 책임 경계가 식별된다
|
||||
|
||||
# 생성 대상
|
||||
|
||||
이 스킬은 아래 구조를 기준으로 scaffold를 제안하거나 생성한다.
|
||||
|
||||
- CLAUDE.md
|
||||
- GEMINI.md
|
||||
- .cursorrules
|
||||
- agent-ops/rules/global.md
|
||||
- agent-ops/rules/_templates/domain-rule-template.md
|
||||
- agent-ops/rules/domain/<domain>/rules.md
|
||||
- agent-ops/skills/router.md
|
||||
- agent-ops/skills/_templates/skill-template.md
|
||||
|
||||
필요 시 아래 초기 skill도 함께 제안한다.
|
||||
|
||||
- agent-ops/skills/find-domain/SKILL.md
|
||||
- agent-ops/skills/code-change/SKILL.md
|
||||
|
||||
# 에이전트 진입 파일 원칙
|
||||
|
||||
CLAUDE.md, GEMINI.md, .cursorrules 는 공통적으로 얇게 유지한다.
|
||||
|
||||
각 파일에는 아래만 둔다.
|
||||
|
||||
- 응답 언어
|
||||
- 기존 구조 우선
|
||||
- 새 파일 생성보다 기존 수정 우선
|
||||
- 작업 전 agent-ops/skills/router.md 참조
|
||||
- 관련 rule / skill 만 최소 로드
|
||||
- 세부 규칙은 agent-ops 아래 파일을 따른다
|
||||
|
||||
각 에이전트 진입 파일에 프로젝트별 상세 규칙을 중복 작성하지 않는다.
|
||||
|
||||
# Rule 구조 원칙
|
||||
|
||||
## global.md
|
||||
전 프로젝트 공통 규칙만 둔다.
|
||||
|
||||
포함 항목:
|
||||
- 응답 언어
|
||||
- 기존 구조 우선
|
||||
- 새 파일 생성보다 기존 수정 우선
|
||||
- 코드 변경 전 관련 domain rule 확인
|
||||
- 요청 범위를 넘는 변경 금지
|
||||
- 불확실하면 단정하지 말고 후보 제시
|
||||
- 플랫폼 특화 규칙은 global에 두지 않음
|
||||
|
||||
## domain rule
|
||||
각 도메인 rules.md 는 아래만 둔다.
|
||||
|
||||
- 목적 / 책임
|
||||
- 포함 경로
|
||||
- 제외 경로
|
||||
- 주요 구성 요소
|
||||
- 유지할 패턴
|
||||
- 다른 도메인과의 경계
|
||||
- 금지 사항
|
||||
|
||||
# DDD 기준
|
||||
|
||||
도메인은 아래 기준으로 분류한다.
|
||||
|
||||
## Core Domain
|
||||
핵심 가치와 주요 유즈케이스를 담당하는 영역
|
||||
|
||||
## Supporting Domain
|
||||
핵심 도메인을 지원하는 영역
|
||||
|
||||
## Generic / Common
|
||||
여러 도메인이 공통으로 사용하는 범용 영역
|
||||
|
||||
도메인은 반드시 실제 폴더, 모듈, 패키지, 네임스페이스, 책임 경계와 연결되어야 한다.
|
||||
|
||||
# Skill 구조 원칙
|
||||
|
||||
## router.md
|
||||
router.md 는 작업 유형에 따라 필요한 skill 또는 domain rule만 로드하게 한다.
|
||||
|
||||
최소 라우팅 축:
|
||||
- 구조 분석 / 스캐폴드
|
||||
- 도메인 판별
|
||||
- 코드 변경
|
||||
- 흐름 추적
|
||||
- 문서/릴리즈 정리
|
||||
|
||||
처음부터 너무 많은 분기를 만들지 않는다.
|
||||
|
||||
## skill-template.md
|
||||
각 skill 은 아래 형식을 따른다.
|
||||
|
||||
- 목적
|
||||
- 언제 호출할지
|
||||
- 입력
|
||||
- 먼저 확인할 것
|
||||
- 실행 절차
|
||||
- 출력 형식
|
||||
- 금지 사항
|
||||
|
||||
# 신규 프로젝트 처리
|
||||
|
||||
신규 프로젝트일 때는 템플릿 중심으로 세팅한다.
|
||||
|
||||
1. 언어/구조를 확인한다.
|
||||
2. 에이전트 진입 파일 3개를 얇게 생성한다.
|
||||
3. agent-ops/rules/global.md 생성
|
||||
4. domain-rule-template.md 생성
|
||||
5. skills/router.md 생성
|
||||
6. skill-template.md 생성
|
||||
7. 핵심 domain placeholder 2~4개만 제안
|
||||
8. 초기 skill 은 최소 2개만 제안한다.
|
||||
|
||||
신규 프로젝트에서는 domain rules 를 과도하게 채우지 않는다.
|
||||
|
||||
# 운영중 프로젝트 처리
|
||||
|
||||
운영중 프로젝트일 때는 현재 구조 기반으로 자동 채움한다.
|
||||
|
||||
1. 구조를 분석한다.
|
||||
2. Core / Supporting / Generic 도메인 후보를 식별한다.
|
||||
3. 실제 경로를 반영한 domain rules 초안을 제안한다.
|
||||
4. 반복 작업 기반으로 필요한 초기 skill 을 제안한다.
|
||||
5. router.md 에 기본 라우팅을 채운다.
|
||||
6. 신규 프로젝트보다 한 단계 더 구체적으로 scaffold 한다.
|
||||
|
||||
운영중 프로젝트에서는 실제 코드 구조에 맞는 domain / skill 을 일부 채워 넣는다.
|
||||
|
||||
# 출력 형식
|
||||
|
||||
## 상태 판별
|
||||
- Agent-Ops 상태: 없음 / 부분 적용 / 운영중
|
||||
- 프로젝트 상태: 신규 / 운영중
|
||||
- 근거: 짧게 요약
|
||||
|
||||
## 스캐폴드 계획
|
||||
- 생성할 파일
|
||||
- 바로 채울 파일
|
||||
- placeholder 로 둘 파일
|
||||
|
||||
## 도메인 제안
|
||||
- Core Domain
|
||||
- Supporting Domain
|
||||
- Generic / Common
|
||||
|
||||
## 초기 skill 제안
|
||||
- skill 이름
|
||||
- 필요한 이유
|
||||
|
||||
## 주의사항
|
||||
- 지금 만들지 말아야 할 것
|
||||
- 아직 확정하면 안 되는 것
|
||||
|
||||
# 금지
|
||||
|
||||
- 플랫폼 전용 규칙을 기본 scaffold 에 넣지 않는다.
|
||||
- agent 별 md 에 상세 규칙을 중복 작성하지 않는다.
|
||||
- 실제 구조보다 앞선 추상 구조를 강요하지 않는다.
|
||||
- 처음부터 많은 domain / skill 을 만들지 않는다.
|
||||
- scaffold 단계에서 불필요한 세부 구현까지 생성하지 않는다.
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
# iOS Simulator Setup Guide for AI Agents
|
||||
|
||||
This guide documents the process for setting up and running the Mattermost Mobile app on an iOS simulator.
|
||||
|
||||
## Prerequisites Check
|
||||
|
||||
Before starting, verify these are installed:
|
||||
|
||||
```bash
|
||||
# Check Xcode
|
||||
xcodebuild -version
|
||||
|
||||
# Check CocoaPods
|
||||
pod --version
|
||||
|
||||
# Check Node.js
|
||||
node --version
|
||||
|
||||
# Check npm dependencies
|
||||
test -d node_modules && echo "node_modules exists" || echo "Run: npm install"
|
||||
```
|
||||
|
||||
## Step 1: iOS Simulator Runtime
|
||||
|
||||
Check if iOS simulators are available:
|
||||
|
||||
```bash
|
||||
xcrun simctl list devices available
|
||||
```
|
||||
|
||||
If no devices are listed or you see "Install Started", you need to download the iOS platform:
|
||||
|
||||
```bash
|
||||
xcodebuild -downloadPlatform iOS
|
||||
```
|
||||
|
||||
**Note:** This downloads ~8GB and takes several minutes. The command runs in the foreground and shows progress.
|
||||
|
||||
## Step 2: CocoaPods Installation
|
||||
|
||||
### Critical: Set UTF-8 Encoding
|
||||
|
||||
CocoaPods requires UTF-8 encoding. Always set this before running pod commands:
|
||||
|
||||
```bash
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
```
|
||||
|
||||
### Install Pods (Apple Silicon Mac)
|
||||
|
||||
```bash
|
||||
cd ios
|
||||
RCT_NEW_ARCH_ENABLED=0 arch -x86_64 pod install
|
||||
```
|
||||
|
||||
### Common CocoaPods Issues
|
||||
|
||||
#### Issue: "Unicode Normalization not appropriate for ASCII-8BIT"
|
||||
**Solution:** Set UTF-8 encoding as shown above.
|
||||
|
||||
#### Issue: Podfile.lock version conflicts
|
||||
**Solution:** Clean install:
|
||||
```bash
|
||||
cd ios
|
||||
rm -rf Pods Podfile.lock build
|
||||
export LANG=en_US.UTF-8 && export LC_ALL=en_US.UTF-8
|
||||
RCT_NEW_ARCH_ENABLED=0 arch -x86_64 pod install
|
||||
```
|
||||
|
||||
#### Issue: GitHub SSH timeouts during pod install
|
||||
**Symptom:** Errors like "Connection to github.com port 22: Operation timed out"
|
||||
|
||||
**Solution:** Temporarily configure git to use HTTPS instead of SSH:
|
||||
```bash
|
||||
git config --global url."https://github.com/".insteadOf git@github.com:
|
||||
```
|
||||
|
||||
**IMPORTANT:** Revert this after pod install completes, or the user won't be able to push:
|
||||
```bash
|
||||
git config --global --unset url.https://github.com/.insteadof
|
||||
```
|
||||
|
||||
**Warning:** Never leave git URL rewrites in place without informing the user!
|
||||
|
||||
## Step 3: Start Metro Bundler (if not already running)
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
Wait for "Dev server ready" message before proceeding.
|
||||
|
||||
## Step 4: Run iOS App (if not already built)
|
||||
|
||||
### Using npm script
|
||||
```bash
|
||||
npm run ios -- --simulator="iPhone 17 Pro"
|
||||
```
|
||||
|
||||
### Or specify a different simulator
|
||||
List available simulators first:
|
||||
```bash
|
||||
xcrun simctl list devices available | grep -E "iPhone|iPad"
|
||||
```
|
||||
|
||||
Then run with your chosen device:
|
||||
```bash
|
||||
npm run ios -- --simulator="iPhone 16e"
|
||||
```
|
||||
|
||||
## Build Times
|
||||
|
||||
- **First build:** 10-30 minutes (compiles all native code)
|
||||
- **Subsequent builds:** Much faster (incremental)
|
||||
- **JS/TS changes:** ~3 seconds hot reload (no native rebuild needed)
|
||||
|
||||
## Troubleshooting Build Failures
|
||||
|
||||
### Getting Better Error Messages
|
||||
|
||||
If `npm run ios` fails, run xcodebuild directly for clearer errors:
|
||||
|
||||
```bash
|
||||
xcodebuild -workspace ios/Mattermost.xcworkspace \
|
||||
-configuration Debug \
|
||||
-scheme Mattermost \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
|
||||
2>&1 | grep -E "error:|fatal error"
|
||||
```
|
||||
|
||||
### Clean Build
|
||||
|
||||
If builds fail mysteriously, try a clean build:
|
||||
|
||||
```bash
|
||||
# Clean Xcode derived data
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/Mattermost-*
|
||||
|
||||
# Clean and reinstall pods
|
||||
cd ios
|
||||
rm -rf Pods build
|
||||
export LANG=en_US.UTF-8 && export LC_ALL=en_US.UTF-8
|
||||
RCT_NEW_ARCH_ENABLED=0 arch -x86_64 pod install
|
||||
```
|
||||
|
||||
### Opening in Xcode
|
||||
|
||||
For complex build issues, open in Xcode for better debugging:
|
||||
|
||||
```bash
|
||||
open ios/Mattermost.xcworkspace
|
||||
```
|
||||
|
||||
Then select your simulator target and press Cmd+B to build.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
# Full setup sequence (Apple Silicon)
|
||||
export LANG=en_US.UTF-8 && export LC_ALL=en_US.UTF-8
|
||||
cd ios && RCT_NEW_ARCH_ENABLED=0 arch -x86_64 pod install && cd ..
|
||||
npm start &
|
||||
sleep 10
|
||||
npm run ios -- --simulator="iPhone 17 Pro"
|
||||
```
|
||||
|
||||
## Do NOT Modify
|
||||
|
||||
- **Git configuration:** Never permanently change git URL rewrites without reverting
|
||||
- **Podfile:** Don't add modular_headers to pods unless you understand the implications
|
||||
- **New Architecture:** Keep `RCT_NEW_ARCH_ENABLED=0` as the project uses the old architecture
|
||||
369
CLAUDE.md
|
|
@ -1,3 +1,366 @@
|
|||
# 필수 준수 사항
|
||||
- agent-ops/rules/global.md 파일을 읽고 이에 대해 꼭 준수해서 진행해야한다.
|
||||
- global.md 내용을 모르면 추정하지 말고 먼저 해당 파일을 확인한다.
|
||||
# CLAUDE.md
|
||||
|
||||
## Project Overview
|
||||
|
||||
React Native 0.76.9 with **New Architecture disabled** (RCT_NEW_ARCH_ENABLED=0).
|
||||
|
||||
## Important Development Notes
|
||||
- Always run the checks before committing code
|
||||
- Always test any changes using the mobile mcp before calling something complete or committing code
|
||||
- Never commit any planning markdown files
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Implementing Multi-Phase Features
|
||||
When implementing complex features with multiple phases:
|
||||
|
||||
1. **Use subagents for each phase**: Launch a subagent (Task tool with `subagent_type: general-purpose`) to implement each phase independently
|
||||
2. **Test after each phase**: Use a subagent to test the feature with mobile MCP after implementation
|
||||
3. **Commit after each phase**: Create a commit for each completed and tested phase
|
||||
4. **Never commit planning files**: Keep `.md` planning files (like `PLAN.md`) out of commits
|
||||
|
||||
### Testing with Mobile MCP
|
||||
When testing features that require mobile device interaction:
|
||||
- **Use subagents**: Launch a subagent to handle mobile MCP testing to avoid excessive context usage
|
||||
- The subagent can launch the app, navigate, take screenshots, and verify functionality
|
||||
- **Don't just verify the app launches** - actually trigger and interact with the feature being tested (e.g., send messages, click buttons, verify state changes)
|
||||
|
||||
## Key Commands
|
||||
|
||||
```bash
|
||||
# Setup
|
||||
npm run pod-install # iOS CocoaPods (or pod-install-m1 for M1 Macs)
|
||||
npm run ios-gems # Ruby gems for iOS (or ios-gems-m1 for M1 Macs)
|
||||
|
||||
# Development
|
||||
npm start # Start Metro bundler
|
||||
npm run ios # Run iOS in simulator
|
||||
npm run android # Run Android in emulator
|
||||
|
||||
# TypeScript type checking
|
||||
npm run tsc
|
||||
|
||||
# Fix linting issues
|
||||
npm run fix
|
||||
|
||||
# E2E tests are in separate detox/ package
|
||||
npm run e2e:ios # cd detox && npm run e2e:ios-test
|
||||
npm run e2e:android # cd detox && npm run e2e:android-build && npm run e2e:android-test
|
||||
|
||||
# Builds delegate to Fastlane via shell scripts
|
||||
npm run build:ios # ./scripts/build.sh ipa
|
||||
npm run build:android # ./scripts/build.sh apk
|
||||
|
||||
# Utilities
|
||||
npm run clean # Clean build artifacts
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Dual Database System (WatermelonDB)
|
||||
**Critical**: Two separate SQLite databases via WatermelonDB:
|
||||
|
||||
1. **App Database**: One global database for app state, server list (`app/database/models/app/`)
|
||||
2. **Server Databases**: One database per connected server for channels, users, posts (`app/database/models/server/`)
|
||||
|
||||
**DatabaseManager** singleton (`app/database/manager/index.ts`) manages all instances. Data operations go through operators with transformers/handlers/comparators.
|
||||
|
||||
**Database directory**:
|
||||
- iOS: App Group directory (shared with extensions)
|
||||
- Android: `${documentDirectory}/databases/`
|
||||
|
||||
### Network Layer
|
||||
- **NetworkManager** (`app/managers/network_manager.ts`) creates **one Client instance per server**
|
||||
- Uses `@mattermost/react-native-network-client`
|
||||
- **Product-specific API calls** should live in `client/rest.ts` within the product folder using the **ClientMix pattern** (see `app/products/calls/client/rest.ts` or `app/products/agents/client/rest.ts` for examples). Don't call `client.doFetch()` directly from action files.
|
||||
|
||||
### Actions Pattern
|
||||
- **Local Actions** (`app/actions/local/`): Database-only operations
|
||||
- **Remote Actions** (`app/actions/remote/`): Fetch from API → use operators to persist → return `{error}` on failure
|
||||
|
||||
### Query Layer
|
||||
**Query layer** (`app/queries/`) provides both:
|
||||
- `query*`: Returns WatermelonDB Query
|
||||
- `observe*`: Returns RxJS Observable
|
||||
- `get*`: Returns Promise (async)
|
||||
- `prepare*`: Returns prepared records for batch operations
|
||||
|
||||
### State Management
|
||||
**Hybrid approach**:
|
||||
- **WatermelonDB**: Primary data store (persisted)
|
||||
- **Ephemeral Stores** (`app/store/`): In-memory transient UI state
|
||||
|
||||
### Navigation
|
||||
- Uses **`react-native-navigation`** v7 (not react-navigation)
|
||||
- Each screen is a separate registered component
|
||||
- Every independently shown/hidden component must be registered as a screen for proper lifecycle management
|
||||
|
||||
### Products Architecture
|
||||
Modular features in `app/products/` with their own database models:
|
||||
- `agents/`: AI agents
|
||||
- `calls/`: Voice/video calling
|
||||
- `playbooks/`: Playbooks with dedicated DB models
|
||||
|
||||
#### Agents Streaming Architecture
|
||||
**WebSocket Events:**
|
||||
- Agents use `custom_mattermost-ai_postupdate` WebSocket events for streaming (start, message updates, reasoning, tool calls, annotations, end)
|
||||
- NO specific WebSocket event for tool call status changes after approval/rejection
|
||||
- Updates arrive via standard `POST_EDITED` event after backend executes tools
|
||||
|
||||
**Critical Pattern:**
|
||||
- Components must clear local streaming state on `ENDED` event to switch from ephemeral streaming data to persisted database data
|
||||
- Otherwise stale streaming state prevents POST_EDITED updates from displaying
|
||||
|
||||
### Custom Native Modules
|
||||
Located at `libraries/@mattermost/`:
|
||||
- `@mattermost/rnutils` - Native utilities (orientation, notifications)
|
||||
- `@mattermost/rnshare` - Share extension integration
|
||||
- `@mattermost/hardware-keyboard` - External keyboard detection
|
||||
- `@mattermost/secure-pdf-viewer` - Secure PDF viewing
|
||||
|
||||
**Native Bridge patterns** (when modifying native modules):
|
||||
- iOS: `RCT_EXPORT_MODULE()`, `RCT_EXPORT_METHOD()` in Objective-C++
|
||||
- Android: Extend `ReactContextBaseJavaModule`, use `@ReactMethod`
|
||||
|
||||
### TypeScript Path Aliases
|
||||
```typescript
|
||||
@actions/* → app/actions/*
|
||||
@agents/* → app/products/agents/*
|
||||
@calls/* → app/products/calls/*
|
||||
@database/* → app/database/*
|
||||
@queries/* → app/queries/*
|
||||
@store → app/store/index
|
||||
@share/* → share_extension/*
|
||||
@typings/* → types/*
|
||||
// ... (see tsconfig.json for complete list)
|
||||
```
|
||||
|
||||
### Share Extension
|
||||
**Separate bundle** at `share_extension/` for Android system share. Shares code with main app but runs independently.
|
||||
* iOS has its own native implementation for the Share Extension with Swift and SwiftUI
|
||||
|
||||
**Important:** Share extension on Android uses **React Navigation** (not react-native-navigation like main app).
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Organization
|
||||
- **Jest coverage excludes** `/components/` and `/screens/` directories
|
||||
- Mock database manager at `app/database/manager/__mocks__/index.ts`
|
||||
- **E2E tests** in separate `detox/` package (not in main package.json)
|
||||
- Add mocks to central `setup.ts` file, not individual test files
|
||||
|
||||
### Testing Patterns
|
||||
|
||||
#### Database Testing
|
||||
- Tests use **real in-memory databases** (LokiJS), not mocks
|
||||
- **Critical**: Set `extraLokiOptions: {autosave: false}` to prevent memory leaks from lingering timeouts
|
||||
- Always clean up: `await DatabaseManager.destroyServerDatabase(serverUrl)` in `afterEach`
|
||||
- Initialize database in `beforeEach`: `await DatabaseManager.init([serverUrl])`
|
||||
|
||||
#### Ephemeral Store Testing
|
||||
- Use `jest.resetModules()` between tests to clear singleton state
|
||||
- Require fresh imports after reset: `const Store = require('./store').default;`
|
||||
- Pattern from `app/store/ephemeral_store.test.ts`
|
||||
|
||||
#### WebSocket Testing
|
||||
- Mock connection objects, not the WebSocket class itself
|
||||
- Create mock with methods: `onOpen`, `onClose`, `onError`, `onMessage`, `send`, `readyState`
|
||||
- Use `client.open()` in tests to trigger `onOpen` callback
|
||||
- Pattern from `app/client/websocket/index.test.ts`
|
||||
|
||||
#### Timer Testing
|
||||
- **Always use**: `jest.useFakeTimers({doNotFake: ['nextTick']})` to let promises resolve
|
||||
- Helper: `advanceTimers(ms)` from `test/timer_helpers.ts` advances time AND waits for promises
|
||||
- Never fake `nextTick` or async operations will hang
|
||||
|
||||
#### Component Rendering
|
||||
- Three helpers in `test/intl-test-helper.tsx`:
|
||||
- `renderWithIntl`: Basic internationalization
|
||||
- `renderWithIntlAndTheme`: + Theme context
|
||||
- `renderWithEverything`: + Database + Server URL
|
||||
- Use `renderWithEverything` when components need database access
|
||||
- Wrap async state updates in `act()` when testing React components
|
||||
|
||||
#### Remote Action Testing
|
||||
- Mock `NetworkManager.getClient` in `beforeAll` to return mock client
|
||||
- Mock client should implement all methods used in tests
|
||||
- Test pattern: Setup DB state → Call action → Assert no error and data returned
|
||||
- Pattern from `app/actions/remote/channel.test.ts`
|
||||
|
||||
#### DeviceEventEmitter Testing
|
||||
- Use `DeviceEventEmitter.addListener()` to spy on events
|
||||
- Remove listener in cleanup: `listener.remove()`
|
||||
- Pattern: Store listener callback in jest.fn(), assert it was called with expected data
|
||||
|
||||
### Test Utilities
|
||||
**TestHelper** singleton at `test/test_helper.ts`:
|
||||
- `generateId()`: Deterministic IDs for tests
|
||||
- `setupServerDatabase()`: Complete DB setup with basic entities
|
||||
- `fakeUser()`, `fakeChannel()`, etc.: Entity generators with sensible defaults
|
||||
- `fakeUserModel()`, `fakeChannelModel()`: WatermelonDB model creators
|
||||
|
||||
### Writing Quality Tests
|
||||
|
||||
**Test names:**
|
||||
- Use `it('should...')` format, not `test('happy path')`
|
||||
- Check array lengths in addition to individual items to ensure no extra elements
|
||||
- Test expectations must match actual implementation behavior - if code deletes state immediately, test for `undefined`, not for modified state values
|
||||
|
||||
**Mocking:**
|
||||
- Use `jest.mocked()` for full implementations
|
||||
- Use `(thing as jest.Mock)` type assertion when mocking partial implementations that don't satisfy the full interface (e.g., `NetworkManager.getClient` returning minimal mock client)
|
||||
- Avoid `any` types in tests - use `typeof import('./module').default` for proper typing of dynamically imported modules
|
||||
|
||||
**Don't test:**
|
||||
- JavaScript operators (`===`, `JSON.stringify()`) or library behavior (React, RxJS)
|
||||
- "Mocks calling mocks" - thin wrapper functions that just pass data through
|
||||
- Multiple input variations when behavior is identical (testing 0, 1, 5 items when function doesn't special-case counts)
|
||||
- Duplicate assertions - if test A verifies X, don't add test B that only verifies X
|
||||
- AI-generated tests are heavily scrutinized and often rejected if they don't demonstrate real intended behavior
|
||||
|
||||
**Do test:**
|
||||
- Business logic, error handling, integration points, side effects
|
||||
- One example per code path is sufficient
|
||||
|
||||
## Development Practices
|
||||
|
||||
### TypeScript
|
||||
- Trust well-formed data - don't add defensive null checks if types guarantee existence
|
||||
- Use `??` (nullish coalescing) instead of `||` for fallbacks when appropriate
|
||||
- Avoid non-null assertions (`!`) - use optional chaining: `metadataRes.metadata?.followers?.length ?? 0`
|
||||
- Type function parameters using `ComponentProps<typeof Component>` instead of `as const` workarounds
|
||||
- JSDoc comments must match actual parameter names and types
|
||||
- **Prefer const objects over enums** for better tree-shaking: `export const Status = { Pending: 0, Done: 1 } as const;` with companion type `export type Status = typeof Status[keyof typeof Status];`
|
||||
|
||||
### React Hooks
|
||||
- **Always explain why dependencies are omitted** from exhaustive-deps with specific comments
|
||||
- Don't over-optimize with `useMemo` - only use for expensive calculations or objects passed as props to children
|
||||
- Use `useCallback` for render functions passed to components, not for functions called within render
|
||||
- Move module-level constants outside components
|
||||
- Replace state variables with derived values when possible
|
||||
- Stable references (refs, dispatch functions) don't need to be in dependency arrays but must have eslint-disable comments
|
||||
- React-Native-Reanimated shared values (`.value`) don't cause re-renders and don't need to be dependencies
|
||||
- **Critical**: When modifying existing `useEffect` hooks, you MUST include ALL dependencies used inside the effect, even if the original code had an incomplete dependency array - ESLint's `react-hooks/exhaustive-deps` rule is enforced strictly
|
||||
- **Memoize callbacks for list items**: When rendering lists with `.map()`, avoid inline arrow functions like `onPress={() => handler(item.id)}`. Instead, have the child component accept the ID and call the callback internally, allowing the parent to pass a single memoized callback reference.
|
||||
|
||||
### React Native & UI
|
||||
- Don't create hooks inside render functions - extract as local components
|
||||
- Prefer `Button` components over `TouchableOpacity` when appropriate
|
||||
- Non-memoized inline styles add render stress - define in stylesheet instead
|
||||
- **`StyleSheet.create` is unnecessary** when using `makeStyleSheetFromTheme` - just return the plain object
|
||||
- **Place `getStyleSheet` at file top** (after imports, before interfaces/components) not at the bottom
|
||||
- Use `Platform.select()` for platform-specific values instead of ternaries
|
||||
- StyleProps support nested lists - no need for custom `concatStyles()`
|
||||
- Use `withTiming()` consistently for both states in animations
|
||||
- Test components with long strings to ensure proper text handling
|
||||
- Use constants from `PREFERENCES.THEMES` instead of hardcoded colors
|
||||
- **Use `react-native-reanimated`** instead of React Native's `Animated` API for all animations (better performance, runs on UI thread)
|
||||
- **Use `usePreventDoubleTap` hook** for button press handlers to prevent accidental double submissions
|
||||
- **Use `useServerUrl()` hook** instead of passing `serverUrl` as a prop - it's available via context
|
||||
- **Use existing components**: Check for `<Loading>` instead of `<ActivityIndicator>`, `safeParseJSON()` instead of try/catch JSON.parse
|
||||
- **Parent checks before mounting**: If a child component would return null for empty data, have the parent conditionally render instead (e.g., `{items.length > 0 && <ItemList items={items} />}`)
|
||||
- **Use `@utils/url` utilities**: `tryOpenURL()` instead of `Linking.openURL()`, `getUrlDomain()` with `urlParse` instead of `new URL()`
|
||||
|
||||
### Code Quality & Linting
|
||||
|
||||
**Import Organization:**
|
||||
- Consolidate all imports from the same module into a single statement
|
||||
- Use inline `type` keyword for type imports: `import {SomeValue, type SomeType} from '@module'`
|
||||
- Example: `import {StreamingEvents, type StreamingState} from '@agents/types'`
|
||||
|
||||
**Unused Code:**
|
||||
- Remove unused imports, parameters, interfaces, and variables completely
|
||||
- ESLint enforces strict no-unused-vars - code with unused declarations will fail CI
|
||||
- When a parameter is truly unused, remove it from the function signature
|
||||
|
||||
**Pre-commit:**
|
||||
- Run `npm run fix` to auto-fix linting issues before committing
|
||||
- Pre-commit hook runs ESLint + TypeScript checking automatically
|
||||
|
||||
### Error Handling & Logging
|
||||
- Use `logError()` instead of `console.error()` or `console.log()`
|
||||
- Use `logDebug()` for debug-level information
|
||||
- Don't ignore potential errors silently - handle them or add intentional comments
|
||||
- Don't log sensitive information
|
||||
- **Add function/class prefix to logs**: e.g., `logError('[ClassName.methodName]', error)` to make debugging easier
|
||||
|
||||
### State Management
|
||||
- Always handle errors from database operations
|
||||
- Consider race conditions in async functions within effects
|
||||
- Local state initialized with prop values won't update when props change - sync with `useEffect` if needed
|
||||
|
||||
### Performance
|
||||
- Create parsers/expensive objects only once, not on every render (use refs or useMemo)
|
||||
- Memoize AST output to avoid regenerating on every render
|
||||
- Use named constants instead of magic numbers and check if one already exists.
|
||||
|
||||
### Localization (i18n)
|
||||
- **CRITICAL**: Only update `en.json` - never modify other language files or Weblate gets corrupted
|
||||
- Default messages in code must match JSON translations exactly, including newlines
|
||||
- Translation IDs should be descriptive enough for translators to understand context
|
||||
- Translate user-facing strings, not debug/error messages
|
||||
|
||||
### Markdown Component Usage
|
||||
**Required props:**
|
||||
- `baseTextStyle`, `value`, `theme`, `location`
|
||||
- Use `Screens.CHANNEL` or appropriate constant from `@constants` for the `location` prop
|
||||
- `textStyles`, `blockStyles`, `enableLatex`, and similar props are auto-generated via HOC
|
||||
|
||||
**Important Limitations:**
|
||||
- **Cannot embed custom React components inline**: The Markdown component renders CommonMark to native components. You cannot insert custom React components (like badges, icons) inline within the markdown text flow
|
||||
- **Link handling**: Links in markdown go through `openLink()` → deep link system → `tryOpenURL()`. Custom URL schemes (e.g., `citation:`, `action:`) will show error alerts unless you implement a full deep link handler in `app/utils/deep_link/`
|
||||
- **Post-processing not possible**: Unlike web, you cannot post-process the rendered markdown JSX tree to replace markers with React components
|
||||
|
||||
### Adding New Post Types
|
||||
When adding custom post types (e.g., for new products):
|
||||
1. Add the type string to `PostType` union in `types/api/posts.d.ts`
|
||||
2. Define constants in your product's constants file (e.g., `app/products/agents/constants.ts`)
|
||||
3. Use the globally-available `Post` type (defined in `types/api/posts.d.ts`) - no import needed
|
||||
|
||||
### Platform-Specific
|
||||
- iOS: Opt out of iOS 18+ features (liquid glass) until UI is properly addressed
|
||||
- Android: Test on multiple API levels (34, 35, 36) to verify behavior with edge-to-edge changes
|
||||
- Use `Set` instead of `Array` for exception lists (faster, ensures uniqueness)
|
||||
|
||||
### testID Convention
|
||||
Components use hierarchical testIDs: `component.subcomponent.element`
|
||||
- Example: `channel_list.category.CATEGORY.channel_item.ID.display_name`
|
||||
- Example: `channel.post_draft.send_action.send.button`
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
### JavaScript Compatibility
|
||||
- **Don't use `Object.hasOwn()`** - React Native doesn't support ES2022+ features
|
||||
- ❌ `Object.hasOwn(obj, 'key')`
|
||||
- ✅ `'key' in obj`
|
||||
|
||||
### Anti-Patterns
|
||||
- Conditionals in tests for TypeScript satisfaction
|
||||
- Creating new arrays/objects on every render when passed as props
|
||||
- Over-engineering - follow YAGNI principle
|
||||
- Impossible states with boolean + optional parameters that depend on each other
|
||||
- Redundant checks - if `array.length >= 3`, indices 0,1,2 are guaranteed to exist
|
||||
- Verbose error messages that reveal internal system details
|
||||
- Mass assignment risks - whitelist fields instead of sending `Partial<Model>` objects
|
||||
- Duplicate conditional branches that return identical results - consolidate into a single condition (e.g., `if (A && B)` and `if (A && !B)` both returning same thing should be just `if (A)`)
|
||||
|
||||
### Security
|
||||
- Question whether showing secrets with "eye" toggle is a security concern
|
||||
- Don't commit files that likely contain secrets (.env, credentials.json)
|
||||
- Consider pre-populating sensitive fields with `*********` instead of actual values
|
||||
|
||||
## Configuration
|
||||
|
||||
- **Patches**: Applied via `patch-package` on postinstall (`patches/` directory)
|
||||
- **Self-compiled apps**: Require your own Mattermost Push Notification Service
|
||||
- **Self-signed certificates**: Not supported
|
||||
|
||||
## Development Notes
|
||||
|
||||
### Hot Reload & Build Times
|
||||
- **Hot reload**: ~3 seconds for JS/TS changes; does NOT work for native code changes
|
||||
- **Native rebuilds**: 10-30 minutes (avoid unless necessary)
|
||||
- **Pre-commit hook**: Runs ESLint + incremental TypeScript checking
|
||||
|
||||
### Known Issues
|
||||
- Many components require `theme` prop - check for `useTheme()` hook in parent component
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# 필수 준수 사항
|
||||
- agent-ops/rules/global.md 파일을 읽고 이에 대해 꼭 준수해서 진행해야한다.
|
||||
- global.md 내용을 모르면 추정하지 말고 먼저 해당 파일을 확인한다.
|
||||
1
Gemfile
|
|
@ -1,4 +1,3 @@
|
|||
source "https://rubygems.org"
|
||||
|
||||
gem "cocoapods", "1.16.1"
|
||||
gem "rexml", ">= 3.4.2"
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ GEM
|
|||
netrc (0.11.0)
|
||||
nkf (0.2.0)
|
||||
public_suffix (4.0.7)
|
||||
rexml (3.4.4)
|
||||
rexml (3.3.9)
|
||||
ruby-macho (2.5.1)
|
||||
securerandom (0.4.0)
|
||||
typhoeus (1.4.1)
|
||||
|
|
@ -107,7 +107,6 @@ PLATFORMS
|
|||
|
||||
DEPENDENCIES
|
||||
cocoapods (= 1.16.1)
|
||||
rexml (>= 3.4.2)
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.11
|
||||
|
|
|
|||
124
NOTICE.txt
|
|
@ -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
|
||||
|
|
@ -150,42 +135,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
|
||||
|
|
@ -1753,43 +1702,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
|
||||
|
|
@ -2525,42 +2437,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
|
||||
|
|
|
|||
34
README.md
|
|
@ -1,7 +1,7 @@
|
|||
# Mattermost Mobile v2
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (10.11.0+)
|
||||
- **Supported iOS versions:** 16.0+
|
||||
- **Supported iOS versions:** 15.1+
|
||||
- **Supported Android versions:** 7.0+
|
||||
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 21 languages. Learn more at [https://mattermost.com](https://mattermost.com).
|
||||
|
|
@ -12,34 +12,6 @@ We plan on releasing monthly updates with new features - check the [changelog](h
|
|||
|
||||
**Important:** If you self-compile the Mattermost Mobile apps you also need to deploy your own [Mattermost Push Notification Service](https://github.com/mattermost/mattermost-push-proxy/releases).
|
||||
|
||||
# Android 16KB Page Size Support
|
||||
|
||||
**Temporary Requirement:** To comply with Google Play's 16KB page size requirement for Android devices, this project includes a compatibility patch that must be applied before building for Android.
|
||||
|
||||
### When to Apply the Patch
|
||||
|
||||
- **CI/CD Builds:** The patch is automatically applied in GitHub Actions for Android builds
|
||||
- **Local Development:** If you're building Android locally and encounter 16KB page size related issues, run:
|
||||
|
||||
```bash
|
||||
npm run apply-16kb-pagesize-patch
|
||||
```
|
||||
|
||||
This script will:
|
||||
1. Update package dependencies to compatible versions
|
||||
2. Apply necessary code changes for 16KB page size support
|
||||
3. Update patch files for modified dependencies
|
||||
4. Regenerate `package-lock.json`
|
||||
|
||||
### ⚠️ Important Warnings
|
||||
|
||||
- **DO NOT commit the changes** applied by this patch to the repository
|
||||
- **These changes will break iOS builds** if committed
|
||||
- The patch is designed to be applied only during Android CI builds
|
||||
- For local development, revert all changes after building Android
|
||||
|
||||
**Note:** This is a temporary solution until all dependencies natively support 16KB page sizes.
|
||||
|
||||
# How to Contribute
|
||||
|
||||
### Testing
|
||||
|
|
@ -47,7 +19,7 @@ This script will:
|
|||
To help with testing app updates before they're released, you can:
|
||||
|
||||
1. Sign up to be a beta tester
|
||||
- [Android](https://play.google.com/apps/testing/com.tokilabs.mattermost)
|
||||
- [Android](https://play.google.com/apps/testing/com.mattermost.rnbeta)
|
||||
- [iOS](https://testflight.apple.com/join/Q7Rx7K9P) - Open this link from your iOS device
|
||||
2. Install the `Mattermost Beta` app. New updates in the Beta app are released periodically. You will receive a notification when the new updates are available.
|
||||
3. File any bugs you find by filing a [GitHub issue](https://github.com/mattermost/mattermost-mobile/issues) with:
|
||||
|
|
@ -59,7 +31,7 @@ To help with testing app updates before they're released, you can:
|
|||
- Join the [Native Mobile Apps channel](https://community.mattermost.com/core/channels/native-mobile-apps) to see what's new and discuss feedback with other contributors and the core team
|
||||
|
||||
You can leave the Beta testing program at any time:
|
||||
- On Android, [click this link](https://play.google.com/apps/testing/com.tokilabs.mattermost) while logged in with your Google Play email address used to opt-in for the Beta program, then click **Leave the program**.
|
||||
- On Android, [click this link](https://play.google.com/apps/testing/com.mattermost.rnbeta) while logged in with your Google Play email address used to opt-in for the Beta program, then click **Leave the program**.
|
||||
- On iOS, access the `Mattermost Beta` app page in TestFlight and click **Stop Testing**.
|
||||
|
||||
### Contribute Code
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
---
|
||||
name: <domain-name>
|
||||
description: <도메인 한 줄 설명>
|
||||
type: core | supporting | generic
|
||||
---
|
||||
|
||||
# <Domain Name>
|
||||
|
||||
## 목적 / 책임
|
||||
<이 도메인이 담당하는 핵심 책임>
|
||||
|
||||
## 포함 경로
|
||||
- `app/<path>/`
|
||||
- `app/actions/local/<file>.ts`
|
||||
- `app/actions/remote/<file>.ts`
|
||||
|
||||
## 제외 경로
|
||||
- <이 도메인에 속하지 않는 경로>
|
||||
|
||||
## 주요 구성 요소
|
||||
- **<component>**: <역할>
|
||||
- **<component>**: <역할>
|
||||
|
||||
## 유지할 패턴
|
||||
- <이 도메인에서 따라야 할 패턴>
|
||||
|
||||
## 다른 도메인과의 경계
|
||||
- **database 도메인**: DB 조작은 database 도메인 패턴을 따른다
|
||||
- **network 도메인**: API 호출은 ClientMix 패턴을 따른다
|
||||
|
||||
## 금지 사항
|
||||
- <이 도메인에서 하면 안 되는 것>
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
---
|
||||
name: database
|
||||
description: WatermelonDB 이중 DB 시스템 - 앱 DB와 서버별 DB
|
||||
type: supporting
|
||||
---
|
||||
|
||||
# Database Domain
|
||||
|
||||
## 목적 / 책임
|
||||
WatermelonDB 기반 이중 데이터베이스를 관리한다. DatabaseManager 싱글톤, operator, 스키마, 모델을 담당한다.
|
||||
|
||||
## 포함 경로
|
||||
- `app/database/` (전체)
|
||||
- `app/database/manager/` - DatabaseManager 싱글톤
|
||||
- `app/database/models/app/` - 앱 전역 모델 (서버 목록 등)
|
||||
- `app/database/models/server/` - 서버별 모델 (채널, 유저, 포스트 등)
|
||||
- `app/database/operator/` - 데이터 쓰기 operator
|
||||
- `app/database/schema/` - DB 스키마 정의
|
||||
- `docs/database/` - DB 스키마 문서
|
||||
|
||||
## 제외 경로
|
||||
- `app/queries/` (Query Layer는 별도 - 여기서 DB를 읽는다)
|
||||
|
||||
## 주요 구성 요소
|
||||
- **DatabaseManager**: 모든 DB 인스턴스 관리 싱글톤 (`app/database/manager/index.ts`)
|
||||
- **App Database**: 서버 목록 등 전역 앱 상태 (하나만 존재)
|
||||
- **Server Database**: 서버별 채널/유저/포스트 데이터 (서버당 하나)
|
||||
- **Operators**: transformer/handler/comparator 패턴으로 데이터 쓰기
|
||||
- **Schema**: 테이블 정의 및 마이그레이션
|
||||
|
||||
## 유지할 패턴
|
||||
- DB 경로: iOS는 App Group, Android는 `${documentDirectory}/databases/`
|
||||
- Operator를 통해 batch 작업으로 데이터 쓰기
|
||||
- Sync handler는 create/update/delete 전체 라이프사이클 처리 필수
|
||||
- 스키마 변경 시 반드시 버전 번호 증가 + 문서(`docs/database/`) 업데이트
|
||||
- Query Layer(`app/queries/`)는 `query*`, `observe*`, `get*`, `prepare*` 패턴
|
||||
|
||||
## 다른 도메인과의 경계
|
||||
- **모든 도메인**: DB 직접 접근보다 Query Layer(`app/queries/`) 사용 권장
|
||||
- **network 도메인**: remote action이 API 응답을 operator에 전달
|
||||
|
||||
## 금지 사항
|
||||
- 스키마 변경 후 `docs/database/` 문서 미업데이트
|
||||
- Sync handler에서 stale 레코드 삭제 누락 (`prepareDestroyPermanently()` 필수)
|
||||
- 테스트에서 `extraLokiOptions: {autosave: false}` 누락 (메모리 누수)
|
||||
- 테스트 후 `DatabaseManager.destroyServerDatabase()` cleanup 누락
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
---
|
||||
name: messaging
|
||||
description: 채널, 포스트, 스레드, 드래프트 등 핵심 메시징 기능
|
||||
type: core
|
||||
---
|
||||
|
||||
# Messaging Domain
|
||||
|
||||
## 목적 / 책임
|
||||
Mattermost의 핵심 메시징 기능: 채널, 포스트, 스레드, 리액션, 드래프트, 예약 포스트를 담당한다.
|
||||
|
||||
## 포함 경로
|
||||
- `app/actions/local/channel.ts`, `post.ts`, `thread.ts`, `draft.ts`, `reactions.ts`, `scheduled_post.ts`
|
||||
- `app/actions/remote/channel.ts`, `post.ts`, `thread.ts`, `reactions.ts`, `search.ts`
|
||||
- `app/queries/servers/channel.ts`, `post.ts`, `thread.ts`, `drafts.ts`, `scheduled_post.ts`, `reaction.ts`
|
||||
- `app/database/models/server/channel*.ts`, `post.ts`, `thread*.ts`, `draft.ts`, `reaction.ts`, `scheduled_post.ts`
|
||||
- `app/screens/channel/`, `app/screens/thread/`, `app/screens/edit_post/`
|
||||
- `app/components/post_list/`, `app/components/post_draft/`
|
||||
|
||||
## 제외 경로
|
||||
- `app/products/` (products는 각 product 도메인)
|
||||
- `app/actions/remote/user.ts` (user 도메인)
|
||||
|
||||
## 주요 구성 요소
|
||||
- **Channel**: 채널 CRUD, 멤버십, 카테고리 관리
|
||||
- **Post**: 포스트 생성/수정/삭제, 파일 첨부, 리액션
|
||||
- **Thread**: 스레드 팔로우, 언급, 참여자 관리
|
||||
- **Draft**: 임시저장, 예약 포스트
|
||||
- **Search**: 채널/포스트 검색
|
||||
|
||||
## 유지할 패턴
|
||||
- Remote action: `fetchXxx()` → API 호출 → operator로 DB 저장 → `{error}` 반환
|
||||
- Local action: DB 조작만 수행
|
||||
- Sync handler는 create/update/delete 전체 라이프사이클을 처리한다
|
||||
- `fetchMissingProfilesByIds()` 사용 - 직접 `client.getProfilesByIds()` 호출 금지
|
||||
|
||||
## 다른 도메인과의 경계
|
||||
- **database 도메인**: WatermelonDB 스키마 변경 시 `docs/database/server/server.md` 업데이트 필요
|
||||
- **network 도메인**: API 호출은 `client/rest.ts`의 ClientMix 패턴 사용
|
||||
- **ui 도메인**: 화면 렌더링은 ui 도메인, 데이터 로직은 이 도메인
|
||||
|
||||
## 금지 사항
|
||||
- `console.error()` / `console.log()` 직접 사용 (→ `logError()`, `logDebug()` 사용)
|
||||
- `TouchableOpacity` 사용 (→ `Pressable` 사용)
|
||||
- DB sync handler에서 삭제 누락 (stale 레코드는 `prepareDestroyPermanently()` 처리)
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
---
|
||||
name: products-agents
|
||||
description: AI 에이전트 기능 - WebSocket 스트리밍, AI 봇, 툴 호출
|
||||
type: core
|
||||
---
|
||||
|
||||
# Products/Agents Domain
|
||||
|
||||
## 목적 / 책임
|
||||
AI 에이전트 기능을 담당한다. WebSocket 스트리밍, AI 봇 관리, 툴 호출/승인, 어노테이션을 처리한다.
|
||||
|
||||
## 포함 경로
|
||||
- `app/products/agents/` (전체)
|
||||
- `app/products/agents/actions/`
|
||||
- `app/products/agents/client/rest.ts`
|
||||
- `app/products/agents/components/`
|
||||
- `app/products/agents/constants.ts`
|
||||
- `app/products/agents/hooks/`
|
||||
- `app/products/agents/screens/`
|
||||
- `app/products/agents/store/`
|
||||
- `app/products/agents/types/`
|
||||
- `app/products/agents/websocket/`
|
||||
|
||||
## 제외 경로
|
||||
- `app/actions/remote/` (messaging 도메인)
|
||||
- `app/products/calls/` (calls 도메인)
|
||||
|
||||
## 주요 구성 요소
|
||||
- **WebSocket Handler**: `custom_mattermost-ai_postupdate` 이벤트 처리 (start, message, reasoning, tool_call, annotation, end)
|
||||
- **Streaming Store**: 스트리밍 중 ephemeral 상태 관리
|
||||
- **AI Bot**: AI 봇 목록 조회 및 DB 동기화 (`handleAIBots`)
|
||||
- **Tool Call**: 툴 호출 승인/거절 처리
|
||||
- **Client (ClientMix)**: `app/products/agents/client/rest.ts`
|
||||
|
||||
## 유지할 패턴
|
||||
- **WebSocket 스트리밍**: `ENDED` 이벤트에서 로컬 스트리밍 상태를 반드시 초기화 (stale state 방지)
|
||||
- **ClientMix 패턴**: API 호출은 `client/rest.ts` 내에서만 수행, action에서 직접 `doFetch()` 금지
|
||||
- **PostType 추가 시**: `types/api/posts.d.ts`의 `PostType` union에 타입 추가
|
||||
- **Sync handler**: create/update/delete 모두 처리 (`prepareDestroyPermanently()` 필수)
|
||||
|
||||
## 다른 도메인과의 경계
|
||||
- **messaging 도메인**: 포스트 데이터는 `POST_EDITED` WebSocket 이벤트로 수신
|
||||
- **database 도메인**: Agents 전용 DB 모델 사용
|
||||
- **network 도메인**: ClientMix를 통해 NetworkManager와 연결
|
||||
|
||||
## 금지 사항
|
||||
- 툴 호출 상태 변경을 별도 WebSocket 이벤트로 처리 (→ `POST_EDITED`로 수신)
|
||||
- `ENDED` 이벤트 후 스트리밍 상태 미초기화
|
||||
- action 파일에서 직접 `client.doFetch()` 호출
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
---
|
||||
name: products-calls
|
||||
description: 음성/화상 통화 기능
|
||||
type: core
|
||||
---
|
||||
|
||||
# Products/Calls Domain
|
||||
|
||||
## 목적 / 책임
|
||||
음성/화상 통화 기능을 담당한다. 통화 연결, 상태 관리, UI를 처리한다.
|
||||
|
||||
## 포함 경로
|
||||
- `app/products/calls/` (전체)
|
||||
- `app/products/calls/actions/`
|
||||
- `app/products/calls/client/rest.ts`
|
||||
- `app/products/calls/components/`
|
||||
- `app/products/calls/constants.ts`
|
||||
- `app/products/calls/hooks/`
|
||||
- `app/products/calls/screens/`
|
||||
- `app/products/calls/store/`
|
||||
- `app/products/calls/types/`
|
||||
|
||||
## 제외 경로
|
||||
- `app/products/agents/` (agents 도메인)
|
||||
- `app/products/playbooks/` (playbooks 도메인)
|
||||
|
||||
## 주요 구성 요소
|
||||
- **Call Connection**: WebRTC 기반 통화 연결 관리
|
||||
- **Call State**: Ephemeral store로 통화 상태 관리
|
||||
- **Client (ClientMix)**: `app/products/calls/client/rest.ts`
|
||||
|
||||
## 유지할 패턴
|
||||
- **ClientMix 패턴**: API 호출은 `client/rest.ts` 내에서만 수행
|
||||
- **Ephemeral Store**: 통화 상태는 ephemeral store로 관리 (DB 아님)
|
||||
|
||||
## 다른 도메인과의 경계
|
||||
- **messaging 도메인**: 채널 정보는 messaging 도메인 쿼리 사용
|
||||
- **network 도메인**: ClientMix를 통해 NetworkManager와 연결
|
||||
|
||||
## 금지 사항
|
||||
- action 파일에서 직접 `client.doFetch()` 호출
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
---
|
||||
name: ui
|
||||
description: 컴포넌트, 스크린, 네비게이션, 스타일링
|
||||
type: generic
|
||||
---
|
||||
|
||||
# UI Domain
|
||||
|
||||
## 목적 / 책임
|
||||
React Native UI 컴포넌트, 스크린, 네비게이션, 스타일링을 담당한다.
|
||||
|
||||
## 포함 경로
|
||||
- `app/components/` - 재사용 컴포넌트
|
||||
- `app/screens/` - 화면 단위 컴포넌트
|
||||
- `app/hooks/` - 공통 React hooks
|
||||
- `app/utils/` - UI 유틸리티
|
||||
- `share_extension/` - Android 공유 확장 (React Navigation 사용)
|
||||
|
||||
## 제외 경로
|
||||
- `app/products/*/components/` (각 product 도메인)
|
||||
- `app/actions/` (비즈니스 로직은 messaging/product 도메인)
|
||||
|
||||
## 주요 구성 요소
|
||||
- **Navigation**: `react-native-navigation` v7 (메인 앱), `React Navigation` (share extension)
|
||||
- **Components**: 60+ 재사용 컴포넌트
|
||||
- **Screens**: 63+ 화면 컴포넌트 (각각 독립 등록)
|
||||
- **Themes**: `PREFERENCES.THEMES` 상수 사용
|
||||
|
||||
## 유지할 패턴
|
||||
- `Pressable` 사용 (→ `TouchableOpacity` 금지), 항상 pressed 피드백 추가
|
||||
- `typography()` 사용 (→ raw `fontSize`/`fontWeight`/`fontFamily` 금지)
|
||||
- `makeStyleSheetFromTheme` 사용 시 `StyleSheet.create` 불필요
|
||||
- `getStyleSheet` 는 파일 상단(imports 이후)에 위치
|
||||
- 정적 객체(hitSlop 등)는 모듈 레벨 상수로 추출
|
||||
- `FormattedText` 사용 (→ `<Text>{intl.formatMessage(...)}</Text>` 지양)
|
||||
- `useServerUrl()` hook 사용 (→ `serverUrl` prop 전달 지양)
|
||||
- `usePreventDoubleTap` hook 사용 (버튼 중복 클릭 방지)
|
||||
- 리스트 렌더링: 인라인 화살표 함수 금지, 자식 컴포넌트에서 ID 받아 처리
|
||||
- `useDidMount` (→ `useEffect(cb, [])` 대신), `useInitialValue` (→ `useMemo(f, [])` 대신)
|
||||
|
||||
## 다른 도메인과의 경계
|
||||
- **messaging/product 도메인**: UI는 데이터 표시만, 비즈니스 로직은 해당 도메인
|
||||
- **database 도메인**: `observe*` 쿼리를 통해 반응형 데이터 구독
|
||||
|
||||
## 금지 사항
|
||||
- `TouchableOpacity` 사용
|
||||
- raw `fontSize`, `fontWeight`, `fontFamily` 스타일 직접 사용
|
||||
- `Object.hasOwn()` 사용 (ES2022+, RN 미지원 → `'key' in obj` 사용)
|
||||
- `Animated` API 사용 (→ `react-native-reanimated` 사용)
|
||||
- `new URL()` 사용 (→ `urlParse` 사용)
|
||||
- `Linking.openURL()` 직접 사용 (→ `tryOpenURL()` 사용)
|
||||
- `ActivityIndicator` 직접 사용 (→ `<Loading>` 컴포넌트 사용)
|
||||
- 커스텀 React 컴포넌트를 Markdown 컴포넌트 내부에 인라인 삽입
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
---
|
||||
name: global
|
||||
description: 전 프로젝트 공통 규칙
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Global Rules
|
||||
|
||||
## 응답 언어
|
||||
- 한국어로 응답한다.
|
||||
|
||||
## 기존 구조 우선
|
||||
- 새 파일 생성보다 기존 파일 수정을 우선한다.
|
||||
- 기존 패턴과 컨벤션을 따른다.
|
||||
|
||||
## 코드 변경 원칙
|
||||
- 코드 변경 전 관련 domain rule을 확인한다.
|
||||
- 요청 범위를 넘는 변경을 금지한다.
|
||||
- 불확실하면 단정하지 말고 후보로 제시한다.
|
||||
|
||||
## 작업 전 확인
|
||||
- `agent-ops/skills/router.md` 를 먼저 참조하여 필요한 skill 또는 domain rule만 로드한다.
|
||||
- 관련 rule / skill 만 최소로 로드한다.
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
---
|
||||
name: <skill-name>
|
||||
description: <skill 한 줄 설명>
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# <Skill Name>
|
||||
|
||||
## 목적
|
||||
<이 skill이 해결하는 문제>
|
||||
|
||||
## 언제 호출할지
|
||||
- <이 skill을 사용해야 하는 상황>
|
||||
- <이 skill을 사용해야 하는 상황>
|
||||
|
||||
## 입력
|
||||
- `<param>`: <설명> (필수/선택)
|
||||
|
||||
## 먼저 확인할 것
|
||||
- [ ] <사전 확인 항목>
|
||||
- [ ] <사전 확인 항목>
|
||||
|
||||
## 실행 절차
|
||||
1. <단계>
|
||||
2. <단계>
|
||||
3. <단계>
|
||||
|
||||
## 출력 형식
|
||||
```
|
||||
<출력 예시>
|
||||
```
|
||||
|
||||
## 금지 사항
|
||||
- <하면 안 되는 것>
|
||||
- <하면 안 되는 것>
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
---
|
||||
name: code-change
|
||||
description: 코드 변경 절차 - 읽기 → 도메인 rule 확인 → 변경 → 검증
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Code Change
|
||||
|
||||
## 목적
|
||||
코드 변경 시 도메인 규칙을 준수하고 기존 패턴을 따른다.
|
||||
|
||||
## 언제 호출할지
|
||||
- 새 기능을 추가할 때
|
||||
- 버그를 수정할 때
|
||||
- 기존 코드를 수정할 때
|
||||
|
||||
## 입력
|
||||
- `변경 대상`: 파일 경로 또는 기능 설명 (필수)
|
||||
- `변경 내용`: 무엇을 어떻게 변경할지 (필수)
|
||||
|
||||
## 먼저 확인할 것
|
||||
- [ ] `find-domain` skill로 대상 도메인 판별
|
||||
- [ ] 해당 domain rule 로드 (`agent-ops/rules/domain/<domain>/rules.md`)
|
||||
- [ ] 변경할 파일을 Read tool로 먼저 읽기
|
||||
- [ ] 기존 패턴과 컨벤션 파악
|
||||
|
||||
## 실행 절차
|
||||
1. 대상 파일 읽기 (Read tool 사용)
|
||||
2. 도메인 rule의 "유지할 패턴"과 "금지 사항" 확인
|
||||
3. 변경 범위를 요청 범위로 제한 (추가 리팩터링 금지)
|
||||
4. 변경 적용 (Edit tool 우선, 전면 재작성은 Write tool)
|
||||
5. TypeScript 타입 확인 (`npm run tsc`)
|
||||
6. 린팅 확인 (`npm run fix`)
|
||||
|
||||
## 출력 형식
|
||||
변경 후:
|
||||
```
|
||||
변경 파일: <path>
|
||||
변경 내용: <요약>
|
||||
확인 필요: <있으면 명시>
|
||||
```
|
||||
|
||||
## 금지 사항
|
||||
- 파일을 읽기 전에 변경하지 않는다
|
||||
- 요청 범위를 넘는 리팩터링, 주석 추가, 타입 어노테이션 추가
|
||||
- 도메인 rule의 금지 사항 위반
|
||||
- `console.log()` / `console.error()` 직접 사용
|
||||
- 다른 언어 파일 수정 (`en.json` 외 i18n 파일 수정 금지)
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
---
|
||||
name: create-domain-rule
|
||||
description: 새로운 도메인 rules.md 파일을 생성하기 위한 범용 스킬
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Create Domain Rule
|
||||
|
||||
## 목적
|
||||
|
||||
`agent-ops/rules/domain/<domain-name>/rules.md` 파일을 올바른 형식으로 생성한다.
|
||||
실제 프로젝트 폴더 구조를 분석하여 경로와 구성 요소를 자동으로 채운다.
|
||||
생성 후 router.md 의 코드 변경 라우팅 테이블에 항목을 추가한다.
|
||||
|
||||
## 언제 호출할지
|
||||
|
||||
- 새로운 도메인 영역의 코드를 처음 변경하기 전에
|
||||
- 기존 domain rule이 없는 경로에 대해 규칙이 필요할 때
|
||||
- 사용자가 특정 도메인의 rule 파일을 만들어 달라고 요청할 때
|
||||
- agent-ops 초기 scaffold 이후 도메인을 추가 확장할 때
|
||||
|
||||
## 입력
|
||||
|
||||
- `domain-name`: 생성할 도메인 이름, kebab-case (필수)
|
||||
- `domain-type`: `core` / `supporting` / `generic` 중 하나 (필수)
|
||||
- `path-hints`: 이 도메인에 해당하는 경로 힌트 목록 (선택, 없으면 자동 탐색)
|
||||
|
||||
## 먼저 확인할 것
|
||||
|
||||
- [ ] `agent-ops/rules/domain/<domain-name>/rules.md` 가 이미 존재하는지 확인
|
||||
- [ ] `agent-ops/rules/_templates/domain-rule-template.md` 를 읽어 최신 템플릿 형식 파악
|
||||
- [ ] `agent-ops/rules/domain/` 하위 기존 domain rule 목록을 확인하여 중복·유사 도메인 여부 판단
|
||||
|
||||
## 실행 절차
|
||||
|
||||
1. **중복 확인**
|
||||
- `agent-ops/rules/domain/<domain-name>/` 폴더 존재 여부 확인
|
||||
- 책임이 겹치는 기존 domain rule 이 있으면 사용자에게 알리고 중단한다
|
||||
|
||||
2. **경로 탐색**
|
||||
- `path-hints` 가 제공된 경우: 해당 경로를 기준으로 폴더 구조 확인
|
||||
- `path-hints` 가 없는 경우: `domain-name` 과 연관된 폴더명·파일명으로 프로젝트를 탐색
|
||||
- 실제로 존재하는 경로만 포함 경로에 기재한다
|
||||
- 존재하지 않는 경로는 기재하지 않는다
|
||||
|
||||
3. **도메인 분석**
|
||||
- 탐색된 경로의 주요 파일·모듈을 읽어 아래를 도출한다
|
||||
- 도메인의 목적 / 책임 한 줄 요약
|
||||
- 포함 경로 목록
|
||||
- 제외 경로 (인접 도메인과 겹칠 수 있는 경로)
|
||||
- 주요 구성 요소 (파일, 모듈, 클래스, 함수 등)
|
||||
- 유지할 패턴 (네이밍 규칙, 아키텍처 패턴 등)
|
||||
- 다른 도메인과의 경계
|
||||
- 금지 사항
|
||||
|
||||
4. **rules.md 생성**
|
||||
- 경로: `agent-ops/rules/domain/<domain-name>/rules.md`
|
||||
- `domain-rule-template.md` 형식을 따른다
|
||||
- 실제 코드에서 확인된 내용만 기재한다
|
||||
- 불확실한 항목은 `<!-- TODO: 확인 필요 -->` 주석으로 남긴다
|
||||
|
||||
5. **router.md 업데이트**
|
||||
- `agent-ops/skills/router.md` 를 읽는다
|
||||
- 코드 변경 섹션의 경로-도메인 라우팅 테이블에 아래 형식으로 추가한다
|
||||
```
|
||||
| `<path-pattern>` | `agent-ops/rules/domain/<domain-name>/rules.md` |
|
||||
```
|
||||
- 경로 패턴은 실제 포함 경로를 기준으로 작성한다
|
||||
- 기존 테이블 항목을 삭제하거나 재정렬하지 않는다
|
||||
|
||||
6. **결과 보고**
|
||||
- 생성한 파일 경로
|
||||
- router.md 에 추가한 항목
|
||||
- 불확실하여 TODO로 남긴 항목 (해당 시)
|
||||
|
||||
## 출력 형식
|
||||
|
||||
```
|
||||
## 생성 완료
|
||||
|
||||
- Domain Rule 경로: agent-ops/rules/domain/<domain-name>/rules.md
|
||||
- 도메인 유형: <core | supporting | generic>
|
||||
- router.md 추가 경로 패턴: <path-pattern>
|
||||
|
||||
## TODO 항목 (확인 필요)
|
||||
- <불확실하여 직접 확인이 필요한 항목> (해당 시)
|
||||
```
|
||||
|
||||
## 금지 사항
|
||||
|
||||
- 이미 존재하는 rules.md 를 덮어쓰지 않는다
|
||||
- 실제 존재하지 않는 경로를 포함 경로에 넣지 않는다
|
||||
- 추측으로 패턴이나 금지 사항을 채우지 않는다 — 확인된 내용만 기재한다
|
||||
- router.md 의 기존 항목을 삭제하거나 재정렬하지 않는다
|
||||
- 하나의 domain rule 에 여러 독립 도메인의 책임을 묶지 않는다
|
||||
- 코드 변경을 수행하지 않는다 — 이 skill 은 rule 파일 생성만 담당한다
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
---
|
||||
name: create-skill
|
||||
description: 새로운 SKILL.md 파일을 생성하기 위한 범용 스킬
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Create Skill
|
||||
|
||||
## 목적
|
||||
|
||||
`agent-ops/skills/<skill-name>/SKILL.md` 파일을 올바른 형식으로 생성한다.
|
||||
기존 skill-template.md 를 기반으로, 요청 목적에 맞는 내용을 채워 넣는다.
|
||||
생성 후 router.md 에 라우팅 항목을 추가한다.
|
||||
|
||||
## 언제 호출할지
|
||||
|
||||
- 새로운 반복 작업 패턴이 생겨 skill로 정의해야 할 때
|
||||
- 기존 skill이 없는 작업 유형을 처음 수행하기 전에
|
||||
- 사용자가 특정 작업을 skill로 만들어 달라고 요청할 때
|
||||
|
||||
## 입력
|
||||
|
||||
- `skill-name`: 생성할 skill 이름, kebab-case (필수)
|
||||
- `purpose`: 이 skill이 해결하는 문제 한 줄 요약 (필수)
|
||||
- `trigger-cases`: 이 skill을 호출해야 하는 상황 목록 (선택)
|
||||
|
||||
## 먼저 확인할 것
|
||||
|
||||
- [ ] `agent-ops/skills/` 하위에 동일 이름의 디렉터리가 이미 존재하는지 확인
|
||||
- [ ] `agent-ops/skills/router.md` 에 이미 유사한 라우팅 항목이 있는지 확인
|
||||
- [ ] `agent-ops/skills/_templates/skill-template.md` 를 읽어 최신 템플릿 형식 파악
|
||||
|
||||
## 실행 절차
|
||||
|
||||
1. **중복 확인**
|
||||
- `agent-ops/skills/<skill-name>/` 폴더 존재 여부 확인
|
||||
- 기능이 겹치는 기존 skill이 있으면 사용자에게 알리고 중단한다
|
||||
|
||||
2. **목적 분석**
|
||||
- `purpose` 와 `trigger-cases` 를 바탕으로 아래 항목을 도출한다
|
||||
- 언제 호출할지 (2~4개)
|
||||
- 필요한 입력 파라미터
|
||||
- 사전 확인 항목
|
||||
- 실행 절차 (3~7단계)
|
||||
- 출력 형식
|
||||
- 금지 사항
|
||||
|
||||
3. **SKILL.md 생성**
|
||||
- 경로: `agent-ops/skills/<skill-name>/SKILL.md`
|
||||
- `skill-template.md` 형식을 따른다
|
||||
- 프로젝트 특화 내용보다 범용 절차를 우선한다
|
||||
- 절차는 구체적이되 지나치게 세부 구현을 기술하지 않는다
|
||||
|
||||
4. **router.md 업데이트**
|
||||
- `agent-ops/skills/router.md` 를 읽는다
|
||||
- 이 skill이 속할 라우팅 축(구조 분석/코드 변경/흐름 추적 등)을 판단한다
|
||||
- 해당 축에 `→ agent-ops/skills/<skill-name>/SKILL.md` 항목을 추가한다
|
||||
- 기존 라우팅 구조를 깨지 않는다
|
||||
|
||||
5. **결과 보고**
|
||||
- 생성한 파일 경로
|
||||
- router.md 에 추가한 항목
|
||||
- 이 skill이 다루지 않는 범위(필요 시)
|
||||
|
||||
## 출력 형식
|
||||
|
||||
```
|
||||
## 생성 완료
|
||||
|
||||
- SKILL 경로: agent-ops/skills/<skill-name>/SKILL.md
|
||||
- router.md 추가 항목: <라우팅 축> → <skill-name>
|
||||
|
||||
## 주의사항 (해당 시)
|
||||
- <이 skill이 다루지 않는 범위 또는 주의할 점>
|
||||
```
|
||||
|
||||
## 금지 사항
|
||||
|
||||
- 이미 존재하는 skill 을 덮어쓰지 않는다
|
||||
- 프로젝트 특화 경로(예: `app/screens/`)를 skill 본문에 하드코딩하지 않는다
|
||||
- skill 생성과 무관한 코드 파일을 수정하지 않는다
|
||||
- router.md 의 기존 항목을 삭제하거나 재정렬하지 않는다
|
||||
- 하나의 skill 에 여러 독립적인 책임을 묶지 않는다
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
---
|
||||
name: find-domain
|
||||
description: 파일 경로 또는 기능으로 해당 도메인을 판별한다
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Find Domain
|
||||
|
||||
## 목적
|
||||
변경하거나 분석할 코드가 어느 도메인에 속하는지 판별한다.
|
||||
|
||||
## 언제 호출할지
|
||||
- 어떤 파일을 수정해야 하는지 불분명할 때
|
||||
- 기능이 어느 도메인에 속하는지 모를 때
|
||||
- 여러 도메인에 걸친 변경이 필요한지 파악할 때
|
||||
|
||||
## 입력
|
||||
- `경로 또는 기능 설명`: 파일 경로, 컴포넌트 이름, 또는 기능 설명 (필수)
|
||||
|
||||
## 먼저 확인할 것
|
||||
- [ ] `agent-ops/skills/router.md`의 경로 패턴 테이블 확인
|
||||
- [ ] CLAUDE.md의 TypeScript Path Aliases 섹션 확인
|
||||
|
||||
## 실행 절차
|
||||
1. 입력이 파일 경로면 → router.md 테이블과 매칭
|
||||
2. 입력이 기능 설명이면 → 관련 파일을 Glob/Grep으로 탐색
|
||||
3. 도메인 후보를 식별하고 해당 domain rule 로드
|
||||
4. 여러 도메인에 걸친 경우 → 주 도메인 + 영향 도메인 명시
|
||||
|
||||
## 출력 형식
|
||||
```
|
||||
도메인: <domain-name>
|
||||
근거: <판별 근거>
|
||||
관련 rule: agent-ops/rules/domain/<domain>/rules.md
|
||||
영향 도메인: <있으면 명시>
|
||||
```
|
||||
|
||||
## 금지 사항
|
||||
- 도메인을 단정하지 않고 불확실하면 후보를 제시한다
|
||||
- 관련 없는 domain rule까지 로드하지 않는다
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
---
|
||||
name: router
|
||||
description: 작업 유형별 skill / domain rule 라우팅
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Skill Router
|
||||
|
||||
작업 유형에 따라 필요한 skill 또는 domain rule만 로드한다.
|
||||
|
||||
## 라우팅 축
|
||||
|
||||
### 구조 분석 / 스캐폴드
|
||||
→ `agent-ops/skills/find-domain/SKILL.md`
|
||||
→ `agent-ops/rules/global.md`
|
||||
|
||||
### 새 Skill 생성
|
||||
→ `agent-ops/skills/create-skill/SKILL.md`
|
||||
|
||||
### 새 Domain Rule 생성
|
||||
→ `agent-ops/skills/create-domain-rule/SKILL.md`
|
||||
|
||||
### 도메인 판별
|
||||
→ `agent-ops/skills/find-domain/SKILL.md`
|
||||
|
||||
### 코드 변경
|
||||
→ `agent-ops/skills/code-change/SKILL.md`
|
||||
→ 변경 대상 경로에 해당하는 domain rule 로드
|
||||
|
||||
| 경로 패턴 | Domain Rule |
|
||||
|---|---|
|
||||
| `app/products/agents/` | `agent-ops/rules/domain/products-agents/rules.md` |
|
||||
| `app/products/calls/` | `agent-ops/rules/domain/products-calls/rules.md` |
|
||||
| `app/database/`, `app/queries/` | `agent-ops/rules/domain/database/rules.md` |
|
||||
| `app/components/`, `app/screens/`, `app/hooks/` | `agent-ops/rules/domain/ui/rules.md` |
|
||||
| `app/actions/`, 채널/포스트/스레드 관련 | `agent-ops/rules/domain/messaging/rules.md` |
|
||||
|
||||
### 흐름 추적
|
||||
→ 흐름에 관련된 domain rule 로드
|
||||
→ `agent-ops/skills/find-domain/SKILL.md`로 경로 판별 후 진행
|
||||
|
||||
### 문서 / 릴리즈 정리
|
||||
→ `agent-ops/rules/global.md`
|
||||
→ `docs/` 폴더 직접 확인
|
||||
|
||||
## 원칙
|
||||
- 필요한 rule만 로드한다. 전체를 로드하지 않는다.
|
||||
- 불확실한 도메인은 `find-domain` skill로 먼저 판별한다.
|
||||
|
|
@ -104,7 +104,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 +112,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 703
|
||||
versionName "2.36.0"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tokilabs.mattermost;
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.wix.detox.Detox;
|
||||
import com.wix.detox.config.DetoxConfig;
|
||||
|
|
@ -77,7 +77,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"
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ 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;
|
||||
|
|
|
|||
|
|
@ -135,22 +135,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -83,17 +83,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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tokilabs.mattermost
|
||||
package com.mattermost.rnbeta
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tokilabs.mattermost
|
||||
package com.mattermost.rnbeta
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tokilabs.mattermost
|
||||
package com.mattermost.rnbeta
|
||||
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tokilabs.mattermost;
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tokilabs.mattermost;
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tokilabs.mattermost;
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 787 KiB |
|
Before Width: | Height: | Size: 334 KiB After Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 351 KiB |
|
Before Width: | Height: | Size: 345 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 730 KiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 730 KiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 730 KiB |
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 17 KiB |
|
|
@ -5,11 +5,11 @@ buildscript {
|
|||
compileSdkVersion = 35
|
||||
targetSdkVersion = 35
|
||||
supportLibVersion = "35.0.0"
|
||||
kotlinVersion = "2.0.21"
|
||||
kotlinVersion = "1.9.25"
|
||||
kotlin_version = kotlinVersion
|
||||
RNNKotlinVersion = kotlinVersion
|
||||
firebaseVersion = "24.1.0"
|
||||
ndkVersion = "27.1.12297006"
|
||||
ndkVersion = "26.1.10909125"
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
|
|
|||
|
|
@ -1,50 +1,49 @@
|
|||
# This is a Gradle generated file for dependency locking.
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
androidx.databinding:databinding-common:8.7.2=classpath
|
||||
androidx.databinding:databinding-compiler-common:8.7.2=classpath
|
||||
com.android.databinding:baseLibrary:8.7.2=classpath
|
||||
com.android.tools.analytics-library:crash:31.7.2=classpath
|
||||
com.android.tools.analytics-library:protos:31.7.2=classpath
|
||||
com.android.tools.analytics-library:shared:31.7.2=classpath
|
||||
com.android.tools.analytics-library:tracker:31.7.2=classpath
|
||||
androidx.databinding:databinding-common:8.6.0=classpath
|
||||
androidx.databinding:databinding-compiler-common:8.6.0=classpath
|
||||
com.android.databinding:baseLibrary:8.6.0=classpath
|
||||
com.android.tools.analytics-library:crash:31.6.0=classpath
|
||||
com.android.tools.analytics-library:protos:31.6.0=classpath
|
||||
com.android.tools.analytics-library:shared:31.6.0=classpath
|
||||
com.android.tools.analytics-library:tracker:31.6.0=classpath
|
||||
com.android.tools.build.jetifier:jetifier-core:1.0.0-beta10=classpath
|
||||
com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta10=classpath
|
||||
com.android.tools.build:aapt2-proto:8.7.2-12006047=classpath
|
||||
com.android.tools.build:aaptcompiler:8.7.2=classpath
|
||||
com.android.tools.build:apksig:8.7.2=classpath
|
||||
com.android.tools.build:apkzlib:8.7.2=classpath
|
||||
com.android.tools.build:builder-model:8.7.2=classpath
|
||||
com.android.tools.build:builder-test-api:8.7.2=classpath
|
||||
com.android.tools.build:builder:8.7.2=classpath
|
||||
com.android.tools.build:bundletool:1.17.1=classpath
|
||||
com.android.tools.build:gradle-api:8.7.2=classpath
|
||||
com.android.tools.build:gradle-settings-api:8.7.2=classpath
|
||||
com.android.tools.build:gradle:8.7.2=classpath
|
||||
com.android.tools.build:manifest-merger:31.7.2=classpath
|
||||
com.android.tools.build:aapt2-proto:8.6.0-11315950=classpath
|
||||
com.android.tools.build:aaptcompiler:8.6.0=classpath
|
||||
com.android.tools.build:apksig:8.6.0=classpath
|
||||
com.android.tools.build:apkzlib:8.6.0=classpath
|
||||
com.android.tools.build:builder-model:8.6.0=classpath
|
||||
com.android.tools.build:builder-test-api:8.6.0=classpath
|
||||
com.android.tools.build:builder:8.6.0=classpath
|
||||
com.android.tools.build:bundletool:1.16.0=classpath
|
||||
com.android.tools.build:gradle-api:8.6.0=classpath
|
||||
com.android.tools.build:gradle-settings-api:8.6.0=classpath
|
||||
com.android.tools.build:gradle:8.6.0=classpath
|
||||
com.android.tools.build:manifest-merger:31.6.0=classpath
|
||||
com.android.tools.build:transform-api:2.0.0-deprecated-use-gradle-api=classpath
|
||||
com.android.tools.ddms:ddmlib:31.7.2=classpath
|
||||
com.android.tools.layoutlib:layoutlib-api:31.7.2=classpath
|
||||
com.android.tools.lint:lint-model:31.7.2=classpath
|
||||
com.android.tools.lint:lint-typedef-remover:31.7.2=classpath
|
||||
com.android.tools.utp:android-device-provider-ddmlib-proto:31.7.2=classpath
|
||||
com.android.tools.utp:android-device-provider-gradle-proto:31.7.2=classpath
|
||||
com.android.tools.utp:android-device-provider-profile-proto:31.7.2=classpath
|
||||
com.android.tools.utp:android-test-plugin-host-additional-test-output-proto:31.7.2=classpath
|
||||
com.android.tools.utp:android-test-plugin-host-apk-installer-proto:31.7.2=classpath
|
||||
com.android.tools.utp:android-test-plugin-host-coverage-proto:31.7.2=classpath
|
||||
com.android.tools.utp:android-test-plugin-host-emulator-control-proto:31.7.2=classpath
|
||||
com.android.tools.utp:android-test-plugin-host-logcat-proto:31.7.2=classpath
|
||||
com.android.tools.utp:android-test-plugin-host-retention-proto:31.7.2=classpath
|
||||
com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:31.7.2=classpath
|
||||
com.android.tools:annotations:31.7.2=classpath
|
||||
com.android.tools:common:31.7.2=classpath
|
||||
com.android.tools:dvlib:31.7.2=classpath
|
||||
com.android.tools:repository:31.7.2=classpath
|
||||
com.android.tools:sdk-common:31.7.2=classpath
|
||||
com.android.tools:sdklib:31.7.2=classpath
|
||||
com.android:signflinger:8.7.2=classpath
|
||||
com.android:zipflinger:8.7.2=classpath
|
||||
com.android.tools.ddms:ddmlib:31.6.0=classpath
|
||||
com.android.tools.layoutlib:layoutlib-api:31.6.0=classpath
|
||||
com.android.tools.lint:lint-model:31.6.0=classpath
|
||||
com.android.tools.lint:lint-typedef-remover:31.6.0=classpath
|
||||
com.android.tools.utp:android-device-provider-ddmlib-proto:31.6.0=classpath
|
||||
com.android.tools.utp:android-device-provider-gradle-proto:31.6.0=classpath
|
||||
com.android.tools.utp:android-test-plugin-host-additional-test-output-proto:31.6.0=classpath
|
||||
com.android.tools.utp:android-test-plugin-host-apk-installer-proto:31.6.0=classpath
|
||||
com.android.tools.utp:android-test-plugin-host-coverage-proto:31.6.0=classpath
|
||||
com.android.tools.utp:android-test-plugin-host-emulator-control-proto:31.6.0=classpath
|
||||
com.android.tools.utp:android-test-plugin-host-logcat-proto:31.6.0=classpath
|
||||
com.android.tools.utp:android-test-plugin-host-retention-proto:31.6.0=classpath
|
||||
com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:31.6.0=classpath
|
||||
com.android.tools:annotations:31.6.0=classpath
|
||||
com.android.tools:common:31.6.0=classpath
|
||||
com.android.tools:dvlib:31.6.0=classpath
|
||||
com.android.tools:repository:31.6.0=classpath
|
||||
com.android.tools:sdk-common:31.6.0=classpath
|
||||
com.android.tools:sdklib:31.6.0=classpath
|
||||
com.android:signflinger:8.6.0=classpath
|
||||
com.android:zipflinger:8.6.0=classpath
|
||||
com.google.android.gms:strict-version-matcher-plugin:1.2.4=classpath
|
||||
com.google.android:annotations:4.1.1.4=classpath
|
||||
com.google.api.grpc:proto-google-common-protos:2.17.0=classpath
|
||||
|
|
@ -114,29 +113,34 @@ org.glassfish.jaxb:jaxb-runtime:2.3.2=classpath
|
|||
org.glassfish.jaxb:txw2:2.3.2=classpath
|
||||
org.jdom:jdom2:2.0.6=classpath
|
||||
org.jetbrains.intellij.deps:trove4j:1.0.20200330=classpath
|
||||
org.jetbrains.kotlin:kotlin-build-statistics:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-build-tools-api:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-compiler-runner:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-daemon-client:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-gradle-plugin-annotations:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-gradle-plugin-api:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-gradle-plugin-idea-proto:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-gradle-plugin-idea:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-gradle-plugin-model:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-gradle-plugins-bom:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-klib-commonizer-api:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-native-utils:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-android-extensions:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-build-tools-api:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-compiler-runner:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-daemon-client:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-gradle-plugin-annotations:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-gradle-plugin-idea-proto:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-gradle-plugin-idea:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-gradle-plugins-bom:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-native-utils:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-project-model:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-reflect:1.9.20=classpath
|
||||
org.jetbrains.kotlin:kotlin-scripting-common:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-scripting-jvm:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.20=classpath
|
||||
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.20=classpath
|
||||
org.jetbrains.kotlin:kotlin-stdlib:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-tooling-core:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-util-io:2.0.21=classpath
|
||||
org.jetbrains.kotlin:kotlin-util-klib:2.0.21=classpath
|
||||
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.4=classpath
|
||||
org.jetbrains.kotlin:kotlin-stdlib:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-tooling-core:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-util-io:1.9.25=classpath
|
||||
org.jetbrains.kotlin:kotlin-util-klib:1.9.25=classpath
|
||||
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0=classpath
|
||||
org.jetbrains:annotations:23.0.0=classpath
|
||||
org.jvnet.staxex:stax-ex:1.8.1=classpath
|
||||
org.ow2.asm:asm-analysis:9.6=classpath
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {fetchConfigAndLicense} from '@actions/remote/systems';
|
|||
import {Events, Preferences, Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
|
||||
import {getPreauthSecret} from '@init/credentials';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {getServer, getServerByIdentifier} from '@queries/app/servers';
|
||||
|
|
@ -27,7 +26,6 @@ jest.mock('@queries/app/servers');
|
|||
jest.mock('@queries/servers/system');
|
||||
jest.mock('@database/manager');
|
||||
jest.mock('@managers/security_manager');
|
||||
jest.mock('@init/credentials');
|
||||
|
||||
jest.mock('@managers/websocket_manager');
|
||||
jest.mock('@utils/log');
|
||||
|
|
@ -184,54 +182,4 @@ describe('switchToServerAndLogin', () => {
|
|||
expect(SecurityManager.isDeviceJailbroken).toHaveBeenCalledWith('serverUrl');
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should retrieve and use pre-auth secret when reconnecting after logout', async () => {
|
||||
const server = {url: 'serverUrl', displayName: 'Server'} as ServersModel;
|
||||
const config = {DiagnosticId: 'diagId'} as ClientConfig;
|
||||
const license = {} as ClientLicense;
|
||||
const preauthSecret = 'test-secret-123';
|
||||
|
||||
jest.mocked(getServer).mockResolvedValueOnce(server);
|
||||
jest.mocked(getPreauthSecret).mockResolvedValueOnce(preauthSecret);
|
||||
jest.mocked(doPing).mockResolvedValueOnce({});
|
||||
jest.mocked(fetchConfigAndLicense).mockResolvedValueOnce({config, license});
|
||||
jest.mocked(getServerByIdentifier).mockResolvedValueOnce(undefined);
|
||||
|
||||
await Actions.switchToServerAndLogin('serverUrl', theme, intl, jest.fn());
|
||||
|
||||
expect(getPreauthSecret).toHaveBeenCalledWith('serverUrl');
|
||||
expect(doPing).toHaveBeenCalledWith('serverUrl', true, 5000, preauthSecret);
|
||||
});
|
||||
|
||||
it('should work correctly when no pre-auth secret is stored', async () => {
|
||||
const server = {url: 'serverUrl', displayName: 'Server'} as ServersModel;
|
||||
const config = {DiagnosticId: 'diagId'} as ClientConfig;
|
||||
const license = {} as ClientLicense;
|
||||
|
||||
jest.mocked(getServer).mockResolvedValueOnce(server);
|
||||
jest.mocked(getPreauthSecret).mockResolvedValueOnce(undefined);
|
||||
jest.mocked(doPing).mockResolvedValueOnce({});
|
||||
jest.mocked(fetchConfigAndLicense).mockResolvedValueOnce({config, license});
|
||||
jest.mocked(getServerByIdentifier).mockResolvedValueOnce(undefined);
|
||||
|
||||
await Actions.switchToServerAndLogin('serverUrl', theme, intl, jest.fn());
|
||||
|
||||
expect(getPreauthSecret).toHaveBeenCalledWith('serverUrl');
|
||||
expect(doPing).toHaveBeenCalledWith('serverUrl', true, 5000, undefined);
|
||||
});
|
||||
|
||||
it('should pass pre-auth secret to doPing even when ping fails', async () => {
|
||||
const server = {url: 'serverUrl'} as ServersModel;
|
||||
const preauthSecret = 'test-secret-456';
|
||||
|
||||
jest.mocked(getServer).mockResolvedValueOnce(server);
|
||||
jest.mocked(getPreauthSecret).mockResolvedValueOnce(preauthSecret);
|
||||
jest.mocked(doPing).mockResolvedValueOnce({error: 'ping error'});
|
||||
|
||||
await Actions.switchToServerAndLogin('serverUrl', theme, intl, jest.fn());
|
||||
|
||||
expect(getPreauthSecret).toHaveBeenCalledWith('serverUrl');
|
||||
expect(doPing).toHaveBeenCalledWith('serverUrl', true, 5000, preauthSecret);
|
||||
expect(alertServerError).toHaveBeenCalledWith(intl, 'ping error');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {doPing} from '@actions/remote/general';
|
|||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import {Screens} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPreauthSecret} from '@init/credentials';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {getServer, getServerByIdentifier} from '@queries/app/servers';
|
||||
|
|
@ -54,10 +53,7 @@ export async function switchToServerAndLogin(serverUrl: string, theme: Theme, in
|
|||
return;
|
||||
}
|
||||
|
||||
// Retrieve pre-auth secret from keychain if it exists
|
||||
const preauthSecret = await getPreauthSecret(server.url);
|
||||
|
||||
const result = await doPing(server.url, true, 5000, preauthSecret);
|
||||
const result = await doPing(server.url, true);
|
||||
if (result.error) {
|
||||
alertServerError(intl, result.error);
|
||||
callback?.();
|
||||
|
|
|
|||
|
|
@ -5,15 +5,13 @@
|
|||
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {ActionType, Events, Navigation} from '@constants';
|
||||
import {Navigation} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getMyChannel} from '@queries/servers/channel';
|
||||
import {getPostById} from '@queries/servers/post';
|
||||
import {getCommonSystemValues, getTeamHistory} from '@queries/servers/system';
|
||||
import {getTeamChannelHistory} from '@queries/servers/team';
|
||||
import {dismissAllModalsAndPopToRoot, dismissAllModalsAndPopToScreen} from '@screens/navigation';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import {
|
||||
switchToChannel,
|
||||
|
|
@ -29,8 +27,6 @@ import {
|
|||
updateChannelsDisplayName,
|
||||
showUnreadChannelsOnly,
|
||||
updateDmGmDisplayName,
|
||||
deletePostsForChannelsWithAutotranslation,
|
||||
deletePostsForChannel,
|
||||
} from './channel';
|
||||
|
||||
import type {ChannelModel, MyChannelModel, SystemModel} from '@database/models/server';
|
||||
|
|
@ -774,19 +770,19 @@ describe('updateMyChannelFromWebsocket', () => {
|
|||
const serverUrl = 'baseHandler.test.com';
|
||||
const channelId = 'id1';
|
||||
const teamId = 'tId1';
|
||||
const channel: Channel = TestHelper.fakeChannel({
|
||||
const channel: Channel = {
|
||||
id: channelId,
|
||||
team_id: teamId,
|
||||
total_msg_count: 0,
|
||||
delete_at: 0,
|
||||
});
|
||||
const channelMember: ChannelMembership = TestHelper.fakeChannelMember({
|
||||
} as Channel;
|
||||
const channelMember: ChannelMembership = {
|
||||
id: 'id',
|
||||
user_id: 'userid',
|
||||
channel_id: channelId,
|
||||
msg_count: 0,
|
||||
roles: '',
|
||||
});
|
||||
} as ChannelMembership;
|
||||
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init([serverUrl]);
|
||||
|
|
@ -812,27 +808,6 @@ describe('updateMyChannelFromWebsocket', () => {
|
|||
expect(model).toBeDefined();
|
||||
expect(model?.roles).toBe('channel_user');
|
||||
});
|
||||
|
||||
it('calls deletePostsForChannel when autotranslation changes', async () => {
|
||||
const post = TestHelper.fakePost({id: 'postid1', channel_id: channelId, root_id: ''});
|
||||
await operator.handleMyChannel({channels: [channel], myChannels: [channelMember], prepareRecordsOnly: false});
|
||||
await operator.handleChannel({channels: [channel], prepareRecordsOnly: false});
|
||||
await operator.handlePosts({actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL, order: [post.id], posts: [post], prepareRecordsOnly: false});
|
||||
|
||||
await updateMyChannelFromWebsocket(serverUrl, {...channelMember, autotranslation_disabled: true}, false);
|
||||
|
||||
expect(DeviceEventEmitter.emit).toHaveBeenCalledWith(Events.POST_DELETED_FOR_CHANNEL, {serverUrl, channelId, teamId: channel.team_id});
|
||||
});
|
||||
|
||||
it('updates member autotranslation from channelMember', async () => {
|
||||
await operator.handleMyChannel({channels: [channel], myChannels: [channelMember], prepareRecordsOnly: false});
|
||||
await operator.handleChannel({channels: [channel], prepareRecordsOnly: false});
|
||||
|
||||
await updateMyChannelFromWebsocket(serverUrl, {...channelMember, autotranslation_disabled: true}, false);
|
||||
|
||||
const member = await getMyChannel(operator.database, channelId);
|
||||
expect(member?.autotranslationDisabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateChannelInfoFromChannel', () => {
|
||||
|
|
@ -1158,186 +1133,3 @@ describe('updateDmGmDisplayName', () => {
|
|||
expect((channels![1] as ChannelModel).displayName).toBe(`${user2.username}, ${user3.username}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deletePostsForChannel', () => {
|
||||
const serverUrl = 'baseHandler.test.com';
|
||||
let operator: ServerDataOperator;
|
||||
|
||||
const channelId = 'channelid1';
|
||||
const teamId = 'tId1';
|
||||
const channel: Channel = TestHelper.fakeChannel({
|
||||
id: channelId,
|
||||
team_id: teamId,
|
||||
total_msg_count: 0,
|
||||
});
|
||||
const channelMember: ChannelMembership = TestHelper.fakeChannelMember({
|
||||
id: 'id',
|
||||
channel_id: channelId,
|
||||
msg_count: 0,
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init([serverUrl]);
|
||||
operator = DatabaseManager.serverDatabases[serverUrl]!.operator;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await DatabaseManager.destroyServerDatabase(serverUrl);
|
||||
});
|
||||
|
||||
it('handle not found database', async () => {
|
||||
const {models, error} = await deletePostsForChannel('foo', channelId);
|
||||
expect(models).toEqual([]);
|
||||
expect(error).toBeTruthy();
|
||||
});
|
||||
|
||||
it('channel not found', async () => {
|
||||
const {models, error} = await deletePostsForChannel(serverUrl, 'nonexistent');
|
||||
expect(models).toEqual([]);
|
||||
expect(error).toBeFalsy();
|
||||
});
|
||||
|
||||
it('channel with no posts', async () => {
|
||||
await operator.handleChannel({channels: [channel], prepareRecordsOnly: false});
|
||||
await operator.handleMyChannel({channels: [channel], myChannels: [channelMember], prepareRecordsOnly: false});
|
||||
|
||||
const {models, error} = await deletePostsForChannel(serverUrl, channelId);
|
||||
expect(models).toEqual([]);
|
||||
expect(error).toBeFalsy();
|
||||
});
|
||||
|
||||
it('channel with posts - batch written and event emitted', async () => {
|
||||
const post = TestHelper.fakePost({id: 'postid1', channel_id: channelId, root_id: ''});
|
||||
await operator.handleChannel({channels: [channel], prepareRecordsOnly: false});
|
||||
await operator.handleMyChannel({channels: [channel], myChannels: [channelMember], prepareRecordsOnly: false});
|
||||
await operator.handlePosts({
|
||||
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
|
||||
order: [post.id],
|
||||
posts: [post],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const listener = jest.fn();
|
||||
const subscription = DeviceEventEmitter.addListener(Events.POST_DELETED_FOR_CHANNEL, listener);
|
||||
|
||||
const {models, error} = await deletePostsForChannel(serverUrl, channelId);
|
||||
subscription.remove();
|
||||
|
||||
expect(error).toBeFalsy();
|
||||
expect(models.length).toBeGreaterThan(0);
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
expect(listener).toHaveBeenCalledWith({serverUrl, channelId, teamId: channel.team_id});
|
||||
|
||||
const myChannel = await getMyChannel(operator.database, channelId);
|
||||
expect(myChannel?.lastFetchedAt).toBe(0);
|
||||
});
|
||||
|
||||
it('prepareRecordsOnly true - no write and no emit', async () => {
|
||||
const post = TestHelper.fakePost({id: 'postid2', channel_id: channelId, root_id: ''});
|
||||
await operator.handleChannel({channels: [channel], prepareRecordsOnly: false});
|
||||
await operator.handleMyChannel({channels: [channel], myChannels: [channelMember], prepareRecordsOnly: false});
|
||||
await operator.handlePosts({
|
||||
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
|
||||
order: [post.id],
|
||||
posts: [post],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const listener = jest.fn();
|
||||
const subscription = DeviceEventEmitter.addListener(Events.POST_DELETED_FOR_CHANNEL, listener);
|
||||
|
||||
const {models, error} = await deletePostsForChannel(serverUrl, channelId, true);
|
||||
subscription.remove();
|
||||
|
||||
expect(error).toBeFalsy();
|
||||
expect(models.length).toBeGreaterThan(0);
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
const myChannel = await getMyChannel(operator.database, channelId);
|
||||
expect(myChannel?._preparedState).toBe('update');
|
||||
|
||||
// Batch the records to avoid the invariant error
|
||||
await operator.batchRecords([...models], 'test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deletePostsForChannelsWithAutotranslation', () => {
|
||||
const serverUrl = 'baseHandler.test.com';
|
||||
let operator: ServerDataOperator;
|
||||
|
||||
const channelId = 'channelid1';
|
||||
const teamId = 'tId1';
|
||||
const channel: Channel = TestHelper.fakeChannel({
|
||||
id: channelId,
|
||||
team_id: teamId,
|
||||
total_msg_count: 0,
|
||||
autotranslation: true,
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await DatabaseManager.init([serverUrl]);
|
||||
operator = DatabaseManager.serverDatabases[serverUrl]!.operator;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await DatabaseManager.destroyServerDatabase(serverUrl);
|
||||
});
|
||||
|
||||
it('no myChannels with autotranslation - no deletes', async () => {
|
||||
const channelMember: ChannelMembership = TestHelper.fakeChannelMember({
|
||||
id: 'id',
|
||||
channel_id: channelId,
|
||||
msg_count: 0,
|
||||
autotranslation_disabled: true,
|
||||
});
|
||||
const post = TestHelper.fakePost({id: 'postid1', channel_id: channelId, root_id: ''});
|
||||
await operator.handleChannel({channels: [channel], prepareRecordsOnly: false});
|
||||
await operator.handleMyChannel({channels: [channel], myChannels: [channelMember], prepareRecordsOnly: false});
|
||||
await operator.handlePosts({
|
||||
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
|
||||
order: [post.id],
|
||||
posts: [post],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const {models, error} = await deletePostsForChannelsWithAutotranslation(serverUrl);
|
||||
expect(error).toBeUndefined();
|
||||
expect(models).toEqual([]);
|
||||
|
||||
const databasePost = await getPostById(operator.database, post.id);
|
||||
expect(databasePost).toBeDefined();
|
||||
});
|
||||
|
||||
it('myChannels with autotranslation - delete posts', async () => {
|
||||
const channelMember: ChannelMembership = TestHelper.fakeChannelMember({
|
||||
id: 'id',
|
||||
channel_id: channelId,
|
||||
msg_count: 0,
|
||||
autotranslation_disabled: false,
|
||||
});
|
||||
const post = TestHelper.fakePost({id: 'postid1', channel_id: channelId, root_id: ''});
|
||||
await operator.handleChannel({channels: [channel], prepareRecordsOnly: false});
|
||||
await operator.handleMyChannel({channels: [channel], myChannels: [channelMember], prepareRecordsOnly: false});
|
||||
await operator.handlePosts({
|
||||
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
|
||||
order: [post.id],
|
||||
posts: [post],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
const {models, error} = await deletePostsForChannelsWithAutotranslation(serverUrl);
|
||||
expect(error).toBeUndefined();
|
||||
expect(models.length).toBeGreaterThan(0);
|
||||
const databasePost = await getPostById(operator.database, post.id);
|
||||
expect(databasePost).toBeUndefined();
|
||||
});
|
||||
|
||||
it('handle error', async () => {
|
||||
jest.spyOn(DatabaseManager, 'getServerDatabaseAndOperator').mockImplementationOnce(() => {
|
||||
throw new Error('DB error');
|
||||
});
|
||||
const {models, error} = await deletePostsForChannelsWithAutotranslation(serverUrl);
|
||||
expect(models).toEqual([]);
|
||||
expect(error).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
|||