Compare commits
18 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
973ae7d142 | ||
|
|
73b42c4a96 | ||
|
|
ca0e27007f | ||
|
|
34837402dc | ||
|
|
fa550da96a | ||
|
|
884d70ed24 | ||
|
|
f8d2716ad7 | ||
|
|
776a656667 | ||
|
|
f830338640 | ||
|
|
ce66105d18 | ||
|
|
f488718b51 | ||
|
|
d17516e8c7 | ||
|
|
dba4aef5fd | ||
|
|
a807f3a528 | ||
|
|
f9b609c5d1 | ||
|
|
6d80244974 | ||
|
|
747982afcb | ||
|
|
7872b619b2 |
79 changed files with 1552 additions and 409 deletions
|
|
@ -27,7 +27,7 @@ runs:
|
|||
|
||||
- name: ci/checkout-private-repo
|
||||
if: ${{ inputs.sign == 'true' }}
|
||||
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
repository: mattermost/mattermost-mobile-private
|
||||
token: ${{ env.MATTERMOST_BUILD_GH_TOKEN }}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ runs:
|
|||
echo "::endgroup::"
|
||||
|
||||
- name: Cache Pods
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
|
||||
with:
|
||||
path: Pods
|
||||
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ runs:
|
|||
using: composite
|
||||
steps:
|
||||
# The required ruby version is mentioned in '.ruby-version'
|
||||
- uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # v1.180.0
|
||||
- uses: ruby/setup-ruby@32110d4e311bd8996b2a82bf2a43b714ccc91777 # v1.221.0
|
||||
|
||||
- name: ci/setup-fastlane-dependencies
|
||||
shell: bash
|
||||
|
|
@ -16,7 +16,7 @@ runs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: Cache Ruby gems
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ runs:
|
|||
using: composite
|
||||
steps:
|
||||
- name: ci/setup-node
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "npm"
|
||||
|
|
@ -22,7 +22,7 @@ runs:
|
|||
echo "::endgroup::"
|
||||
|
||||
- name: Cache Node.js modules
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
uses: actions/cache@0c907a75c2c80ebcb7f088228285e798b750cf8f # v4.2.1
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
|
|
|
|||
6
.github/workflows/build-android-beta.yml
vendored
6
.github/workflows/build-android-beta.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/prepare-android-build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
|
|
@ -54,7 +54,7 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-android-beta-build
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: android-build-beta-${{ github.run_id }}
|
||||
path: "*.apk"
|
||||
|
|
|
|||
6
.github/workflows/build-android-release.yml
vendored
6
.github/workflows/build-android-release.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/prepare-android-build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
|
|
@ -54,7 +54,7 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-android-release-build
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: android-build-release-${{ github.run_id }}
|
||||
path: "*.apk"
|
||||
|
|
|
|||
10
.github/workflows/build-ios-beta.yml
vendored
10
.github/workflows/build-ios-beta.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
|
@ -44,7 +44,7 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-ios-pr-simulator
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: ios-build-simulator-${{ github.run_id }}
|
||||
path: Mattermost-simulator-x86_64.app.zip
|
||||
|
|
@ -56,7 +56,7 @@ jobs:
|
|||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/output-ssh-private-key
|
||||
shell: bash
|
||||
|
|
@ -93,7 +93,7 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-ios-beta-build
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: ios-build-beta-${{ github.run_id }}
|
||||
path: "*.ipa"
|
||||
|
|
|
|||
10
.github/workflows/build-ios-release.yml
vendored
10
.github/workflows/build-ios-release.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
|
@ -65,7 +65,7 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-ios-release-build
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: ios-build-release-${{ github.run_id }}
|
||||
path: "*.ipa"
|
||||
|
|
@ -77,7 +77,7 @@ jobs:
|
|||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
|
@ -93,7 +93,7 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-ios-pr-simulator
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: ios-build-simulator-${{ github.run_id }}
|
||||
path: Mattermost-simulator-x86_64.app.zip
|
||||
|
|
|
|||
10
.github/workflows/build-pr.yml
vendored
10
.github/workflows/build-pr.yml
vendored
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
if: ${{ github.event.label.name == 'Build Apps for PR' || github.event.label.name == 'Build App for iOS' || github.event.label.name == 'Build App for Android' }}
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: ci/test
|
||||
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-ios-pr-build
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: ios-build-pr-${{ github.run_id }}
|
||||
path: "*.ipa"
|
||||
|
|
@ -72,7 +72,7 @@ jobs:
|
|||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
|
|
@ -94,7 +94,7 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-android-pr-build
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: android-build-pr-${{ github.run_id }}
|
||||
path: "*.apk"
|
||||
|
|
|
|||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -16,6 +16,6 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
|
|
|||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
|
|
@ -26,17 +26,17 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
|
||||
# Autobuild attempts to build any compiled languages
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
run: echo '${{ inputs.CMT_MATRIX }}' > instance-details.json
|
||||
|
||||
- name: cmt/upload-instance-details
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: instance-details.json
|
||||
path: instance-details.json
|
||||
|
|
@ -32,7 +32,7 @@ jobs:
|
|||
MOBILE_SHA: ${{ steps.repo.outputs.MOBILE_SHA }}
|
||||
steps:
|
||||
- name: cmt/checkout-mobile
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ jobs:
|
|||
- update-initial-status
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: Upload iOS Simulator Build
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: ios-build-simulator-${{ github.run_id }}
|
||||
path: Mattermost-simulator-x86_64.app.zip
|
||||
|
|
|
|||
295
.github/workflows/e2e-android-template.yml
vendored
Normal file
295
.github/workflows/e2e-android-template.yml
vendored
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
name: Detox Android E2E Tests Template
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
MM_TEST_SERVER_URL:
|
||||
description: "The test server URL"
|
||||
required: false
|
||||
type: string
|
||||
MM_TEST_USER_NAME:
|
||||
description: "The admin username of the test instance"
|
||||
required: false
|
||||
type: string
|
||||
MM_TEST_PASSWORD:
|
||||
description: "The admin password of the test instance"
|
||||
required: false
|
||||
type: string
|
||||
MOBILE_VERSION:
|
||||
description: "The mobile version to test"
|
||||
required: false
|
||||
default: ${{ github.head_ref || github.ref }}
|
||||
type: string
|
||||
run-android-tests:
|
||||
description: "Run Android tests"
|
||||
required: true
|
||||
type: boolean
|
||||
run-type:
|
||||
type: string
|
||||
required: false
|
||||
default: "PR"
|
||||
testcase_failure_fatal:
|
||||
description: "Should failures be considered fatal"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
record_tests_in_zephyr:
|
||||
description: "Record test results in Zephyr, typically for nightly and release runs"
|
||||
required: false
|
||||
type: string
|
||||
default: 'false'
|
||||
low_bandwidth_mode:
|
||||
description: "Enable low bandwidth mode"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
android_avd_name:
|
||||
description: "Android Emulator name"
|
||||
required: false
|
||||
type: string
|
||||
default: "detox_pixel_4_xl"
|
||||
android_api_level:
|
||||
description: "Android API level"
|
||||
required: false
|
||||
type: string
|
||||
default: "33"
|
||||
outputs:
|
||||
STATUS:
|
||||
value: ${{ jobs.generate-report.outputs.STATUS }}
|
||||
TARGET_URL:
|
||||
value: ${{ jobs.generate-report.outputs.TARGET_URL }}
|
||||
FAILURES:
|
||||
value: ${{ jobs.generate-report.outputs.FAILURES }}
|
||||
|
||||
env:
|
||||
AWS_REGION: "us-east-1"
|
||||
ADMIN_EMAIL: ${{ secrets.MM_MOBILE_E2E_ADMIN_EMAIL }}
|
||||
ADMIN_USERNAME: ${{ secrets.MM_MOBILE_E2E_ADMIN_USERNAME }}
|
||||
ADMIN_PASSWORD: ${{ secrets.MM_MOBILE_E2E_ADMIN_PASSWORD }}
|
||||
BRANCH: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}
|
||||
COMMIT_HASH: ${{ github.sha }}
|
||||
DEVICE_NAME: ${{ inputs.android_avd_name }} # This is needed to split tests as same code is used in iOS job
|
||||
DEVICE_OS_VERSION: ${{ inputs.android_api_level }}
|
||||
DETOX_AWS_S3_BUCKET: "mattermost-detox-report"
|
||||
HEADLESS: "true"
|
||||
TYPE: ${{ inputs.run-type }}
|
||||
PULL_REQUEST: "https://github.com/mattermost/mattermost-mobile/pull/${{ github.event.number }}"
|
||||
SITE_1_URL: ${{ inputs.MM_TEST_SERVER_URL || 'https://mobile-e2e-site-1.test.mattermost.cloud' }}
|
||||
SITE_2_URL: "https://mobile-e2e-site-2.test.mattermost.cloud"
|
||||
SITE_3_URL: "https://mobile-e2e-site-3.test.mattermost.cloud"
|
||||
ZEPHYR_ENABLE: ${{ inputs.record_tests_in_zephyr }}
|
||||
JIRA_PROJECT_KEY: "MM"
|
||||
ZEPHYR_API_KEY: ${{ secrets.MM_MOBILE_E2E_ZEPHYR_API_KEY }}
|
||||
ZEPHYR_FOLDER_ID: "3233873"
|
||||
TEST_CYCLE_LINK_PREFIX: ${{ secrets.MM_MOBILE_E2E_TEST_CYCLE_LINK_PREFIX }}
|
||||
WEBHOOK_URL: ${{ secrets.MM_MOBILE_E2E_WEBHOOK_URL }}
|
||||
FAILURE_MESSAGE: "Something has failed"
|
||||
IOS: "false"
|
||||
RUNNING_E2E: "true"
|
||||
AVD_NAME: ${{ inputs.android_avd_name }}
|
||||
SDK_VERSION: ${{ inputs.android_api_level }}
|
||||
|
||||
jobs:
|
||||
generate-specs:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
specs: ${{ steps.generate-specs.outputs.specs }}
|
||||
build_id: ${{ steps.resolve-device.outputs.BUILD_ID }}
|
||||
mobile_sha: ${{ steps.resolve-device.outputs.MOBILE_SHA }}
|
||||
mobile_ref: ${{ steps.resolve-device.outputs.MOBILE_REF }}
|
||||
workflow_hash: ${{ steps.resolve-device.outputs.WORKFLOW_HASH }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
- name: Set Build ID
|
||||
id: resolve-device
|
||||
run: |
|
||||
BUILD_ID="${{ github.run_id }}-${{ env.AVD_NAME }}-${{ env.SDK_VERSION}}"
|
||||
WORKFLOW_HASH=$(tr -dc a-z0-9 </dev/urandom | head -c 10)
|
||||
|
||||
## We need that hash to separate the artifacts
|
||||
echo "WORKFLOW_HASH=${WORKFLOW_HASH}" >> ${GITHUB_OUTPUT}
|
||||
|
||||
echo "BUILD_ID=$(echo ${BUILD_ID} | sed 's/ /_/g')" >> ${GITHUB_OUTPUT}
|
||||
echo "MOBILE_SHA=$(git rev-parse HEAD)" >> ${GITHUB_OUTPUT}
|
||||
echo "MOBILE_REF=$(git rev-parse --abbrev-ref HEAD)" >> ${GITHUB_OUTPUT}
|
||||
|
||||
- name: Generate Test Specs
|
||||
id: generate-specs
|
||||
uses: ./.github/actions/generate-specs
|
||||
with:
|
||||
parallelism: 10
|
||||
search_path: detox/e2e/test
|
||||
device_name: ${{ env.AVD_NAME }}
|
||||
device_os_version: ${{ env.SDK_VERSION }}
|
||||
|
||||
e2e-android:
|
||||
name: android-detox-e2e-${{ matrix.runId }}-${{ matrix.deviceName }}-${{ matrix.deviceOsVersion }}
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
continue-on-error: true
|
||||
timeout-minutes: 150
|
||||
env:
|
||||
ANDROID_HOME: /usr/local/lib/android/sdk
|
||||
ANDROID_SDK_ROOT: /usr/local/lib/android/sdk
|
||||
needs:
|
||||
- generate-specs
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.generate-specs.outputs.specs) }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libpulse0
|
||||
sudo apt-get install -y scrot ffmpeg xvfb
|
||||
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: Prepare Android Build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
env:
|
||||
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
|
||||
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
|
||||
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
|
||||
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
|
||||
|
||||
- name: Install Detox Dependencies
|
||||
run: |
|
||||
cd detox
|
||||
npm install
|
||||
|
||||
- name: Create destination path
|
||||
run: mkdir -p android/app/build
|
||||
|
||||
- name: Download APK artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: android-build-files-${{ github.run_id }}
|
||||
path: android/app/build
|
||||
|
||||
- name: Set up Android SDK
|
||||
run: |
|
||||
export ANDROID_HOME=/usr/local/lib/android/sdk
|
||||
export PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/emulator:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools:$PATH
|
||||
echo "ANDROID_HOME=$ANDROID_HOME" >> $GITHUB_ENV
|
||||
echo "PATH=$PATH" >> $GITHUB_ENV
|
||||
|
||||
- name: Start Xvfb
|
||||
run: |
|
||||
Xvfb :99 -screen 0 1920x1080x24 &
|
||||
export DISPLAY=:99
|
||||
echo "DISPLAY=:99" >> $GITHUB_ENV
|
||||
|
||||
- name: Accept Android licenses
|
||||
run: |
|
||||
yes | sdkmanager --licenses || true
|
||||
|
||||
- name: Install Android system image
|
||||
run: |
|
||||
sdkmanager "system-images;android-34;default;x86_64"
|
||||
sdkmanager "platform-tools" "emulator"
|
||||
|
||||
- name: Create and run Android Emulator
|
||||
run: |
|
||||
cd detox
|
||||
chmod +x ./create_android_emulator.sh
|
||||
./create_android_emulator.sh ${{ env.SDK_VERSION }} ${{ env.AVD_NAME }} ${{ matrix.specs }}
|
||||
continue-on-error: true # We want to run all the tests
|
||||
|
||||
- name: Upload Android Test Report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: android-results-${{ needs.generate-specs.outputs.workflow_hash }}-${{ matrix.runId }}
|
||||
path: detox/artifacts/
|
||||
|
||||
generate-report:
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- generate-specs
|
||||
- e2e-android
|
||||
outputs:
|
||||
TARGET_URL: ${{ steps.set-url.outputs.TARGET_URL }}
|
||||
STATUS: ${{ steps.determine-status.outputs.STATUS }}
|
||||
FAILURES: ${{ steps.summary.outputs.FAILURES }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
- name: ci/prepare-node-deps
|
||||
uses: ./.github/actions/prepare-node-deps
|
||||
|
||||
- name: Download Android Artifacts
|
||||
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
|
||||
with:
|
||||
path: detox/artifacts/
|
||||
pattern: android-results-${{ needs.generate-specs.outputs.workflow_hash }}-*
|
||||
continue-on-error: true
|
||||
|
||||
- name: Generate Report Path
|
||||
id: s3
|
||||
run: |
|
||||
path="${{ needs.generate-specs.outputs.build_id }}-${{ needs.generate-specs.outputs.mobile_sha }}-${{ needs.generate-specs.outputs.mobile_ref }}"
|
||||
echo "path=$(echo "${path}" | sed 's/\./-/g')" >> ${GITHUB_OUTPUT}
|
||||
|
||||
- name: Save report Detox Dependencies
|
||||
id: report-link
|
||||
run: |
|
||||
cd detox
|
||||
npm ci
|
||||
npm run e2e:save-report
|
||||
env:
|
||||
DETOX_AWS_ACCESS_KEY_ID: ${{ secrets.MM_MOBILE_DETOX_AWS_ACCESS_KEY_ID }}
|
||||
DETOX_AWS_SECRET_ACCESS_KEY: ${{ secrets.MM_MOBILE_DETOX_AWS_SECRET_ACCESS_KEY }}
|
||||
BUILD_ID: ${{ needs.generate-specs.outputs.build_id }}
|
||||
REPORT_PATH: ${{ steps.s3.outputs.path }}
|
||||
## These are needed for the MM Webhook report
|
||||
COMMIT_HASH: ${{ needs.generate-specs.outputs.mobile_sha }}
|
||||
BRANCH: ${{ needs.generate-specs.outputs.mobile_ref }}
|
||||
|
||||
- name: Calculate failures
|
||||
id: summary
|
||||
run: |
|
||||
echo "FAILURES=$(cat detox/artifacts/summary.json | jq .stats.failures)" >> ${GITHUB_OUTPUT}
|
||||
echo "PASSES=$(cat detox/artifacts/summary.json | jq .stats.passes)" >> ${GITHUB_OUTPUT}
|
||||
echo "SKIPPED=$(cat detox/artifacts/summary.json | jq .stats.skipped)" >> ${GITHUB_OUTPUT}
|
||||
echo "TOTAL=$(cat detox/artifacts/summary.json | jq .stats.tests)" >> ${GITHUB_OUTPUT}
|
||||
echo "ERRORS=$(cat detox/artifacts/summary.json | jq .stats.errors)" >> ${GITHUB_OUTPUT}
|
||||
echo "PERCENTAGE=$(cat detox/artifacts/summary.json | jq .stats.passPercent)" >> ${GITHUB_OUTPUT}
|
||||
|
||||
- name: Set Target URL
|
||||
id: set-url
|
||||
run: |
|
||||
echo "TARGET_URL=https://${{ env.DETOX_AWS_S3_BUCKET }}.s3.amazonaws.com/${{ steps.s3.outputs.path }}/jest-stare/android-report.html" >> ${GITHUB_OUTPUT}
|
||||
|
||||
- name: Determine Status
|
||||
id: determine-status
|
||||
run: |
|
||||
if [[ ${{ steps.summary.outputs.failures }} -gt 0 && "${{ inputs.testcase_failure_fatal }}" == "true" ]]; then
|
||||
echo "STATUS=failure" >> ${GITHUB_OUTPUT}
|
||||
else
|
||||
echo "STATUS=success" >> ${GITHUB_OUTPUT}
|
||||
fi
|
||||
|
||||
- name: Generate Summary
|
||||
run: |
|
||||
echo "| Tests | Passed :white_check_mark: | Failed :x: | Skipped :fast_forward: | Errors :warning: | " >> ${GITHUB_STEP_SUMMARY}
|
||||
echo "|:---:|:---:|:---:|:---:|:---:|" >> ${GITHUB_STEP_SUMMARY}
|
||||
echo "| ${{ steps.summary.outputs.TOTAL }} | ${{ steps.summary.outputs.PASSES }} | ${{ steps.summary.outputs.FAILURES }} | ${{ steps.summary.outputs.SKIPPED }} | ${{ steps.summary.outputs.ERRORS }} |" >> ${GITHUB_STEP_SUMMARY}
|
||||
echo "" >> ${GITHUB_STEP_SUMMARY}
|
||||
echo "You can check the full report [here](${{ steps.set-url.outputs.TARGET_URL }})" >> ${GITHUB_STEP_SUMMARY}
|
||||
echo "There was **${{ steps.summary.outputs.PERCENTAGE }}%** success rate." >> ${GITHUB_STEP_SUMMARY}
|
||||
46
.github/workflows/e2e-detox-pr.yml
vendored
46
.github/workflows/e2e-detox-pr.yml
vendored
|
|
@ -34,7 +34,7 @@ jobs:
|
|||
- update-initial-status
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Prepare iOS Build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
|
@ -49,11 +49,53 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: Upload iOS Simulator Build
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: ios-build-simulator-${{ github.run_id }}
|
||||
path: Mattermost-simulator-x86_64.app.zip
|
||||
|
||||
build-android-apk:
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
if: contains(github.event.label.name, 'E2E Android tests for PR')
|
||||
needs:
|
||||
- update-initial-status-android
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_jvmargs: -Xmx8g
|
||||
steps:
|
||||
- name: Prune Docker to free up space
|
||||
run: docker system prune -af
|
||||
|
||||
- name: Remove npm Temporary Files
|
||||
run: |
|
||||
rm -rf ~/.npm/_cacache
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Prepare Android Build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
env:
|
||||
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
|
||||
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
|
||||
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
|
||||
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: sudo apt-get clean && sudo apt-get update && sudo apt-get install -y default-jdk
|
||||
|
||||
- name: Detox build
|
||||
run: |
|
||||
cd detox
|
||||
npm install
|
||||
npm install -g detox-cli
|
||||
npm run e2e:android-build
|
||||
|
||||
- name: Upload Android Build
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: android-build-files-${{ github.run_id }}
|
||||
path: "android/app/build/**/*"
|
||||
|
||||
run-ios-tests-on-pr:
|
||||
name: iOS Mobile Tests on PR
|
||||
uses: ./.github/workflows/e2e-detox-template.yml
|
||||
|
|
|
|||
54
.github/workflows/e2e-detox-release.yml
vendored
54
.github/workflows/e2e-detox-release.yml
vendored
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
- update-initial-status
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Prepare iOS Build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
|
@ -40,11 +40,61 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: Upload iOS Simulator Build
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: ios-build-simulator-${{ github.run_id }}
|
||||
path: Mattermost-simulator-x86_64.app.zip
|
||||
|
||||
build-android-apk:
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
needs:
|
||||
- update-initial-status-android
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_jvmargs: -Xmx8g
|
||||
steps:
|
||||
- name: Prune Docker to free up space
|
||||
run: docker system prune -af
|
||||
|
||||
- name: Remove npm Temporary Files
|
||||
run: |
|
||||
rm -rf ~/.npm/_cacache
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
- name: Prepare Android Build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
env:
|
||||
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
|
||||
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
|
||||
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
|
||||
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: sudo apt-get clean && sudo apt-get update && sudo apt-get install -y default-jdk
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.gradle/caches/modules-2/
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: ${{ runner.os }}-gradle-
|
||||
|
||||
- name: Detox build
|
||||
run: |
|
||||
cd detox
|
||||
npm install
|
||||
npm install -g detox-cli
|
||||
npm run e2e:android-build
|
||||
|
||||
- name: Upload Android Build
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: android-build-files-${{ github.run_id }}
|
||||
path: "android/app/build/**/*"
|
||||
|
||||
run-ios-tests-on-release:
|
||||
name: iOS Mobile Tests on Release
|
||||
uses: ./.github/workflows/e2e-detox-template.yml
|
||||
|
|
|
|||
54
.github/workflows/e2e-detox-scheduled.yml
vendored
54
.github/workflows/e2e-detox-scheduled.yml
vendored
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
- update-initial-status
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Prepare iOS Build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
|
@ -39,11 +39,61 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: Upload iOS Simulator Build
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: ios-build-simulator-${{ github.run_id }}
|
||||
path: Mattermost-simulator-x86_64.app.zip
|
||||
|
||||
build-android-apk:
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
needs:
|
||||
- update-initial-status-android
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_jvmargs: -Xmx8g
|
||||
steps:
|
||||
- name: Prune Docker to free up space
|
||||
run: docker system prune -af
|
||||
|
||||
- name: Remove npm Temporary Files
|
||||
run: |
|
||||
rm -rf ~/.npm/_cacache
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
- name: Prepare Android Build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
env:
|
||||
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
|
||||
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
|
||||
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
|
||||
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: sudo apt-get clean && sudo apt-get update && sudo apt-get install -y default-jdk
|
||||
|
||||
- name: Cache Gradle dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.gradle/caches/modules-2/
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: ${{ runner.os }}-gradle-
|
||||
|
||||
- name: Detox build
|
||||
run: |
|
||||
cd detox
|
||||
npm install
|
||||
npm install -g detox-cli
|
||||
npm run e2e:android-build
|
||||
|
||||
- name: Upload Android Build
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: android-build-files-${{ github.run_id }}
|
||||
path: "android/app/build/**/*"
|
||||
|
||||
run-ios-tests-on-main-scheduled:
|
||||
name: iOS Mobile Tests on Main (Scheduled)
|
||||
uses: ./.github/workflows/e2e-detox-template.yml
|
||||
|
|
|
|||
10
.github/workflows/e2e-detox-template.yml
vendored
10
.github/workflows/e2e-detox-template.yml
vendored
|
|
@ -97,7 +97,7 @@ jobs:
|
|||
workflow_hash: ${{ steps.resolve-device.outputs.WORKFLOW_HASH }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
|
|
@ -138,7 +138,7 @@ jobs:
|
|||
matrix: ${{ fromJSON(needs.generate-specs.outputs.specs) }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
|
|
@ -208,7 +208,7 @@ jobs:
|
|||
|
||||
- name: Upload mitmdump Flow Output
|
||||
if: ${{ inputs.low_bandwidth_mode }}
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: ios-mitmdump-flow-output-${{ needs.generate-specs.outputs.workflow_hash }}-${{ matrix.runId }}
|
||||
path: |
|
||||
|
|
@ -217,7 +217,7 @@ jobs:
|
|||
|
||||
- name: Upload iOS Test Report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: ios-results-${{ needs.generate-specs.outputs.workflow_hash }}-${{ matrix.runId }}
|
||||
path: detox/artifacts/
|
||||
|
|
@ -234,7 +234,7 @@ jobs:
|
|||
FAILURES: ${{ steps.summary.outputs.FAILURES }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.MOBILE_VERSION }}
|
||||
|
||||
|
|
|
|||
14
.github/workflows/github-release.yml
vendored
14
.github/workflows/github-release.yml
vendored
|
|
@ -10,7 +10,7 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: ci/test
|
||||
uses: ./.github/actions/test
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/prepare-ios-build
|
||||
uses: ./.github/actions/prepare-ios-build
|
||||
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-ios-unsigned
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
path: Mattermost-unsigned.ipa
|
||||
name: Mattermost-unsigned.ipa
|
||||
|
|
@ -53,7 +53,7 @@ jobs:
|
|||
- test
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: ci/prepare-android-build
|
||||
uses: ./.github/actions/prepare-android-build
|
||||
|
|
@ -68,7 +68,7 @@ jobs:
|
|||
working-directory: ./fastlane
|
||||
|
||||
- name: ci/upload-android-unsigned-build
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
path: Mattermost-unsigned.apk
|
||||
name: Mattermost-unsigned.apk
|
||||
|
|
@ -80,9 +80,9 @@ jobs:
|
|||
- build-android-unsigned
|
||||
steps:
|
||||
- name: ci/checkout-repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- uses: ruby/setup-ruby@ff740bc00a01b3a50fffc55a1071b1060eeae9dc # v1.180.0
|
||||
- uses: ruby/setup-ruby@32110d4e311bd8996b2a82bf2a43b714ccc91777 # v1.221.0
|
||||
|
||||
- name: release/setup-fastlane-dependencies
|
||||
run: bundle install
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Mattermost Mobile v2
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (9.11.0+)
|
||||
- **Supported iOS versions:** 13.4+
|
||||
- **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).
|
||||
|
|
|
|||
|
|
@ -114,8 +114,8 @@ android {
|
|||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 602
|
||||
versionName "2.25.0"
|
||||
versionCode 620
|
||||
versionName "2.26.2"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,20 +9,18 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.Person;
|
||||
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
import com.mattermost.helpers.*;
|
||||
import com.mattermost.turbolog.TurboLog;
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
import com.wix.reactnativenotifications.core.notification.INotificationsApplication;
|
||||
import com.wix.reactnativenotifications.core.notification.PushNotificationProps;
|
||||
|
||||
public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
|
|
@ -30,15 +28,6 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
|||
private Bundle bundle;
|
||||
private NotificationManager notificationManager;
|
||||
|
||||
private ReactApplicationContext getReactContext(Context context) {
|
||||
if (context instanceof ReactApplication) {
|
||||
ReactNativeHost host = ((ReactApplication) context).getReactNativeHost();
|
||||
return (ReactApplicationContext) host.getReactInstanceManager().getCurrentReactContext();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
try {
|
||||
|
|
@ -80,7 +69,6 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
|||
WritableMap headers = Arguments.createMap();
|
||||
headers.putString("Content-Type", "application/json");
|
||||
|
||||
|
||||
WritableMap body = Arguments.createMap();
|
||||
body.putString("channel_id", channelId);
|
||||
body.putString("message", message.toString());
|
||||
|
|
@ -92,9 +80,19 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
|||
|
||||
String postsEndpoint = "/api/v4/posts?set_online=false";
|
||||
Network.post(serverUrl, postsEndpoint, options, new ResolvePromise() {
|
||||
private boolean isSuccessful(int statusCode) {
|
||||
return statusCode >= 200 && statusCode < 300;
|
||||
}
|
||||
@Override
|
||||
public void resolve(@Nullable Object value) {
|
||||
if (value != null) {
|
||||
ReadableMap response = (ReadableMap)value;
|
||||
ReadableMap data = response.getMap("data");
|
||||
if (data != null && data.hasKey("status_code") && !isSuccessful(data.getInt("status_code"))) {
|
||||
TurboLog.Companion.i("ReactNative", String.format("Reply FAILED exception %s", data.getString("message")));
|
||||
onReplyFailed(notificationId);
|
||||
return;
|
||||
}
|
||||
onReplySuccess(notificationId, message);
|
||||
TurboLog.Companion.i("ReactNative", "Reply SUCCESS");
|
||||
} else {
|
||||
|
|
@ -104,13 +102,13 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void reject(Throwable reason) {
|
||||
public void reject(@NonNull Throwable reason) {
|
||||
TurboLog.Companion.i("ReactNative", String.format("Reply FAILED exception %s", reason.getMessage()));
|
||||
onReplyFailed(notificationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, String message) {
|
||||
public void reject(@NonNull String code, String message) {
|
||||
TurboLog.Companion.i("ReactNative",
|
||||
String.format("Reply FAILED status %s BODY %s", code, message)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ public class ReceiptDelivery {
|
|||
JSONObject jsonResponse = new JSONObject(responseBody);
|
||||
return parseAckResponse(jsonResponse);
|
||||
} catch (Exception e) {
|
||||
TurboLog.Companion.e("ReactNative", "Send receipt delivery failed " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ include ':reactnativenotifications'
|
|||
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/lib/android/app')
|
||||
include ':watermelondb-jsi'
|
||||
project(':watermelondb-jsi').projectDir = new File(rootProject.projectDir, '../node_modules/@nozbe/watermelondb/native/android-jsi')
|
||||
include ':frameanimation'
|
||||
project(':frameanimation').projectDir = new File('../node_modules/APNG4Android/frameanimation')
|
||||
include ':gif'
|
||||
project(':gif').projectDir = new File('../node_modules/APNG4Android/gif')
|
||||
includeBuild('../node_modules/@react-native/gradle-plugin')
|
||||
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle")
|
||||
useExpoModules()
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export async function handleBindingClick<Res=unknown>(serverUrl: string, binding
|
|||
id: 'apps.error.malformed_binding',
|
||||
defaultMessage: 'This binding is not properly formed. Contact the App developer.',
|
||||
});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
return {error: makeCallErrorResponse<Res>(errMsg)};
|
||||
}
|
||||
|
||||
const res: AppCallResponse<Res> = {
|
||||
|
|
@ -48,7 +48,7 @@ export async function handleBindingClick<Res=unknown>(serverUrl: string, binding
|
|||
id: 'apps.error.malformed_binding',
|
||||
defaultMessage: 'This binding is not properly formed. Contact the App developer.',
|
||||
});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
return {error: makeCallErrorResponse<Res>(errMsg)};
|
||||
}
|
||||
|
||||
const callRequest = createCallRequest(
|
||||
|
|
@ -83,7 +83,7 @@ export async function doAppSubmit<Res=unknown>(serverUrl: string, inCall: AppCal
|
|||
id: 'apps.error.responses.form.no_form',
|
||||
defaultMessage: 'Response type is `form`, but no valid form was included in response.',
|
||||
});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
return {error: makeCallErrorResponse<Res>(errMsg)};
|
||||
}
|
||||
|
||||
cleanForm(res.form);
|
||||
|
|
@ -96,7 +96,7 @@ export async function doAppSubmit<Res=unknown>(serverUrl: string, inCall: AppCal
|
|||
id: 'apps.error.responses.navigate.no_url',
|
||||
defaultMessage: 'Response type is `navigate`, but no url was included in response.',
|
||||
});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
return {error: makeCallErrorResponse<Res>(errMsg)};
|
||||
}
|
||||
|
||||
return {data: res};
|
||||
|
|
@ -107,7 +107,7 @@ export async function doAppSubmit<Res=unknown>(serverUrl: string, inCall: AppCal
|
|||
}, {
|
||||
type: responseType,
|
||||
});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
return {error: makeCallErrorResponse<Res>(errMsg)};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -116,7 +116,7 @@ export async function doAppSubmit<Res=unknown>(serverUrl: string, inCall: AppCal
|
|||
defaultMessage: 'Received an unexpected error.',
|
||||
});
|
||||
logDebug('error on doAppSubmit', getFullErrorMessage(error));
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
return {error: makeCallErrorResponse<Res>(errMsg)};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ export async function doAppFetchForm<Res=unknown>(serverUrl: string, call: AppCa
|
|||
id: 'apps.error.responses.form.no_form',
|
||||
defaultMessage: 'Response type is `form`, but no valid form was included in response.',
|
||||
});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
return {error: makeCallErrorResponse<Res>(errMsg)};
|
||||
}
|
||||
cleanForm(res.form);
|
||||
return {data: res};
|
||||
|
|
@ -144,7 +144,7 @@ export async function doAppFetchForm<Res=unknown>(serverUrl: string, call: AppCa
|
|||
id: 'apps.error.responses.unknown_type',
|
||||
defaultMessage: 'App response type not supported. Response type: {type}.',
|
||||
}, {type: responseType});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
return {error: makeCallErrorResponse<Res>(errMsg)};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -153,7 +153,7 @@ export async function doAppFetchForm<Res=unknown>(serverUrl: string, call: AppCa
|
|||
defaultMessage: 'Received an unexpected error.',
|
||||
});
|
||||
logDebug('error on doAppFetchForm', getFullErrorMessage(error));
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
return {error: makeCallErrorResponse<Res>(errMsg)};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +175,7 @@ export async function doAppLookup<Res=unknown>(serverUrl: string, call: AppCallR
|
|||
id: 'apps.error.responses.unknown_type',
|
||||
defaultMessage: 'App response type not supported. Response type: {type}.',
|
||||
}, {type: responseType});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
return {error: makeCallErrorResponse<Res>(errMsg)};
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
|
|
@ -184,7 +184,7 @@ export async function doAppLookup<Res=unknown>(serverUrl: string, call: AppCallR
|
|||
defaultMessage: 'Received an unexpected error.',
|
||||
});
|
||||
logDebug('error on doAppLookup', getFullErrorMessage(error));
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
return {error: makeCallErrorResponse<Res>(errMsg)};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,14 @@
|
|||
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
import {createIntl} from 'react-intl';
|
||||
import {Alert, DeviceEventEmitter, Platform} from 'react-native';
|
||||
|
||||
import {Events} from '@constants';
|
||||
import {GLOBAL_IDENTIFIERS, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {logWarning} from '@utils/log';
|
||||
|
||||
import {
|
||||
addPushProxyVerificationStateFromLogin,
|
||||
|
|
@ -25,6 +28,11 @@ import {
|
|||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type {LoginArgs} from '@typings/database/database';
|
||||
|
||||
const intl = createIntl({
|
||||
locale: 'en',
|
||||
messages: {},
|
||||
});
|
||||
|
||||
const serverUrl = 'baseHandler.test.com';
|
||||
let operator: ServerDataOperator;
|
||||
|
||||
|
|
@ -45,6 +53,7 @@ const mockClient = {
|
|||
getSessions: jest.fn(() => [session1]),
|
||||
sendPasswordResetEmail: jest.fn(() => ({status: 200})),
|
||||
getMe: jest.fn(() => user1),
|
||||
logout: jest.fn(),
|
||||
};
|
||||
|
||||
let mockGetPushProxyVerificationState: jest.Mock;
|
||||
|
|
@ -90,6 +99,14 @@ jest.mock('@utils/security', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('@utils/log', () => {
|
||||
const original = jest.requireActual('@utils/log');
|
||||
return {
|
||||
...original,
|
||||
logWarning: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
|
|
@ -194,12 +211,6 @@ describe('sessions', () => {
|
|||
expect(result.failed).toBe(false);
|
||||
});
|
||||
|
||||
it('logout - base case', async () => {
|
||||
const result = await logout(serverUrl, true, true, true);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.data).toBeDefined();
|
||||
});
|
||||
|
||||
it('cancelSessionNotification - handle not found database', async () => {
|
||||
const result = await cancelSessionNotification('foo');
|
||||
expect(result).toBeDefined();
|
||||
|
|
@ -370,3 +381,100 @@ describe('sessions', () => {
|
|||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('logout', () => {
|
||||
const mockEmit = jest.spyOn(DeviceEventEmitter, 'emit').mockImplementation(() => true);
|
||||
const mockAlert = jest.spyOn(Alert, 'alert').mockImplementation(() => true);
|
||||
|
||||
type TestCase = {
|
||||
options: {
|
||||
skipServerLogout: boolean;
|
||||
logoutOnAlert: boolean;
|
||||
removeServer: boolean;
|
||||
skipEvents: boolean;
|
||||
};
|
||||
withIntl: boolean;
|
||||
clientReturnError: boolean;
|
||||
}
|
||||
|
||||
const testCases: TestCase[] = [];
|
||||
for (const skipServerLogout of [false, true]) {
|
||||
for (const logoutOnAlert of [false, true]) {
|
||||
for (const removeServer of [false, true]) {
|
||||
for (const skipEvents of [false, true]) {
|
||||
for (const withIntl of [false, true]) {
|
||||
for (const clientReturnError of [false, true]) {
|
||||
testCases.push({options: {
|
||||
skipServerLogout,
|
||||
logoutOnAlert,
|
||||
removeServer,
|
||||
skipEvents},
|
||||
withIntl,
|
||||
clientReturnError});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test.each(testCases)('%j', async ({clientReturnError, options, withIntl}) => {
|
||||
if (clientReturnError) {
|
||||
mockClient.logout.mockImplementationOnce(() => {
|
||||
throw new Error('logout error');
|
||||
});
|
||||
} else {
|
||||
mockClient.logout = jest.fn(() => ({status: 'OK'}));
|
||||
}
|
||||
|
||||
const mockFormatMessage = jest.spyOn(intl, 'formatMessage');
|
||||
|
||||
const shouldCallClient = !options.skipServerLogout;
|
||||
const shouldLogWarning = shouldCallClient && clientReturnError;
|
||||
const shouldShowAlert = shouldCallClient && clientReturnError;
|
||||
const shouldEmit = !options.skipEvents;
|
||||
const shouldEmitBeforeAlert = shouldEmit && (!shouldShowAlert || options.logoutOnAlert);
|
||||
const shouldEmitAfterAlert = shouldEmit && !shouldEmitBeforeAlert;
|
||||
const expectedResult = !(shouldShowAlert && !options.logoutOnAlert);
|
||||
|
||||
const clientCalls = shouldCallClient ? 1 : 0;
|
||||
const alertButtons = options.logoutOnAlert ? 1 : 2;
|
||||
|
||||
const result = await logout(serverUrl, withIntl ? intl : undefined, {...options});
|
||||
|
||||
expect(result.data).toBe(expectedResult);
|
||||
expect(mockClient.logout).toHaveBeenCalledTimes(clientCalls);
|
||||
if (shouldLogWarning) {
|
||||
expect(logWarning).toHaveBeenCalled();
|
||||
}
|
||||
|
||||
if (shouldEmitBeforeAlert) {
|
||||
expect(mockEmit).toHaveBeenCalledWith(Events.SERVER_LOGOUT, {serverUrl, removeServer: options.removeServer});
|
||||
} else {
|
||||
expect(mockEmit).not.toHaveBeenCalled();
|
||||
}
|
||||
if (shouldShowAlert) {
|
||||
if (withIntl) {
|
||||
expect(mockFormatMessage).toHaveBeenCalled();
|
||||
}
|
||||
expect(mockAlert).toHaveBeenCalled();
|
||||
expect(mockAlert.mock.calls[0][2]).toHaveLength(alertButtons);
|
||||
if (alertButtons === 2) {
|
||||
mockAlert.mock.calls[0][2]?.[0].onPress?.();
|
||||
expect(mockClient.logout).toHaveBeenCalledTimes(clientCalls);
|
||||
expect(mockEmit).toHaveBeenCalledTimes(0);
|
||||
}
|
||||
|
||||
mockAlert.mock.calls[0][2]?.[alertButtons - 1].onPress?.(); // Last button should be confirm
|
||||
expect(mockClient.logout).toHaveBeenCalledTimes(clientCalls);
|
||||
if (shouldEmitAfterAlert) {
|
||||
expect(mockEmit).toHaveBeenCalledWith(Events.SERVER_LOGOUT, {serverUrl, removeServer: options.removeServer});
|
||||
} else {
|
||||
expect(mockEmit).toHaveBeenCalledTimes(shouldEmitBeforeAlert ? 1 : 0);
|
||||
}
|
||||
} else {
|
||||
expect(mockFormatMessage).not.toHaveBeenCalled();
|
||||
expect(mockAlert).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import NetInfo from '@react-native-community/netinfo';
|
||||
import {DeviceEventEmitter, Platform} from 'react-native';
|
||||
import {defineMessages, type IntlShape} from 'react-intl';
|
||||
import {Alert, DeviceEventEmitter, Platform, type AlertButton} from 'react-native';
|
||||
|
||||
import {Database, Events} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
|
|
@ -25,6 +26,33 @@ import type {LoginArgs} from '@typings/database/database';
|
|||
|
||||
const HTTP_UNAUTHORIZED = 401;
|
||||
|
||||
const logoutMessages = defineMessages({
|
||||
title: {
|
||||
id: 'logout.fail.title',
|
||||
defaultMessage: 'Logout not complete',
|
||||
},
|
||||
bodyForced: {
|
||||
id: 'logout.fail.message.forced',
|
||||
defaultMessage: 'We could not log you out of the server. Some data may continue to be accessible to this device once the device goes back online.',
|
||||
},
|
||||
body: {
|
||||
id: 'logout.fail.message',
|
||||
defaultMessage: 'You’re not fully logged out. Some data may continue to be accessible to this device once the device goes back online. What do you want to do?',
|
||||
},
|
||||
cancel: {
|
||||
id: 'logout.fail.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
continue: {
|
||||
id: 'logout.fail.continue_anyway',
|
||||
defaultMessage: 'Continue Anyway',
|
||||
},
|
||||
ok: {
|
||||
id: 'logout.fail.ok',
|
||||
defaultMessage: 'OK',
|
||||
},
|
||||
});
|
||||
|
||||
export const addPushProxyVerificationStateFromLogin = async (serverUrl: string) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
@ -56,7 +84,7 @@ export const forceLogoutIfNecessary = async (serverUrl: string, err: unknown) =>
|
|||
const currentUserId = await getCurrentUserId(database);
|
||||
|
||||
if (isErrorWithStatusCode(err) && err.status_code === HTTP_UNAUTHORIZED && isErrorWithUrl(err) && err.url?.indexOf('/login') === -1 && currentUserId) {
|
||||
await logout(serverUrl);
|
||||
await logout(serverUrl, undefined, {skipServerLogout: true});
|
||||
return {error: null, logout: true};
|
||||
}
|
||||
|
||||
|
|
@ -135,15 +163,61 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
|
|||
}
|
||||
};
|
||||
|
||||
export const logout = async (serverUrl: string, skipServerLogout = false, removeServer = false, skipEvents = false) => {
|
||||
type LogoutOptions = {
|
||||
skipServerLogout?: boolean;
|
||||
removeServer?: boolean;
|
||||
skipEvents?: boolean;
|
||||
logoutOnAlert?: boolean;
|
||||
};
|
||||
|
||||
export const logout = async (
|
||||
serverUrl: string,
|
||||
intl: IntlShape | undefined,
|
||||
{
|
||||
skipServerLogout = false,
|
||||
removeServer = false,
|
||||
skipEvents = false,
|
||||
logoutOnAlert = false,
|
||||
}: LogoutOptions = {}) => {
|
||||
if (!skipServerLogout) {
|
||||
let loggedOut = false;
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
await client.logout();
|
||||
const response = await client.logout();
|
||||
if (response.status === 'OK') {
|
||||
loggedOut = true;
|
||||
}
|
||||
} catch (error) {
|
||||
// We want to log the user even if logging out from the server failed
|
||||
logWarning('An error occurred logging out from the server', serverUrl, getFullErrorMessage(error));
|
||||
}
|
||||
|
||||
if (!loggedOut) {
|
||||
const title = intl?.formatMessage(logoutMessages.title) || logoutMessages.title.defaultMessage;
|
||||
|
||||
const bodyMessage = logoutOnAlert ? logoutMessages.bodyForced : logoutMessages.body;
|
||||
const confirmMessage = logoutOnAlert ? logoutMessages.ok : logoutMessages.continue;
|
||||
const body = intl?.formatMessage(bodyMessage) || bodyMessage.defaultMessage;
|
||||
const cancel = intl?.formatMessage(logoutMessages.cancel) || logoutMessages.cancel.defaultMessage;
|
||||
const confirm = intl?.formatMessage(confirmMessage) || confirmMessage.defaultMessage;
|
||||
|
||||
const buttons: AlertButton[] = logoutOnAlert ? [] : [{text: cancel, style: 'cancel'}];
|
||||
buttons.push({
|
||||
text: confirm,
|
||||
onPress: logoutOnAlert ? undefined : () => {
|
||||
logout(serverUrl, intl, {skipEvents, removeServer, logoutOnAlert, skipServerLogout: true});
|
||||
},
|
||||
});
|
||||
Alert.alert(
|
||||
title,
|
||||
body,
|
||||
buttons,
|
||||
);
|
||||
|
||||
if (!logoutOnAlert) {
|
||||
return {data: false};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!skipEvents) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {t} from '@i18n';
|
|||
import {setServerCredentials} from '@init/credentials';
|
||||
import PerformanceMetricsManager from '@managers/performance_metrics_manager';
|
||||
import {NetworkRequestMetrics} from '@managers/performance_metrics_manager/constant';
|
||||
import {isErrorWithStatusCode} from '@utils/errors';
|
||||
import {getFormattedFileSize} from '@utils/file';
|
||||
import {logDebug, logInfo} from '@utils/log';
|
||||
import {semverFromServerVersion} from '@utils/server';
|
||||
|
|
@ -376,6 +377,7 @@ export default class ClientTracking {
|
|||
try {
|
||||
response = await request!(url, this.buildRequestOptions(options));
|
||||
} catch (error) {
|
||||
const status_code = isErrorWithStatusCode(error) ? error.status_code : undefined;
|
||||
throw new ClientError(this.apiClient.baseUrl, {
|
||||
message: 'Received invalid response from the server.',
|
||||
intl: {
|
||||
|
|
@ -384,6 +386,7 @@ export default class ClientTracking {
|
|||
},
|
||||
url,
|
||||
details: error,
|
||||
status_code,
|
||||
});
|
||||
} finally {
|
||||
if (groupLabel && CollectNetworkMetrics) {
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ export class ParsedCommand {
|
|||
};
|
||||
|
||||
private findBindings(b: AppBinding) {
|
||||
return b.label.toLowerCase() === this.incomplete.toLowerCase();
|
||||
return b.label?.toLowerCase() === this.incomplete.toLowerCase();
|
||||
}
|
||||
|
||||
// matchBinding finds the closest matching command binding.
|
||||
|
|
@ -321,7 +321,7 @@ export class ParsedCommand {
|
|||
let field = fields.find((f: AppField) => f.position === this.position);
|
||||
if (!field) {
|
||||
field = fields.find((f) => f.position === -1 && f.type === AppFieldTypes.TEXT);
|
||||
if (!field || this.values[field.name]) {
|
||||
if (!field?.name || this.values[field.name]) {
|
||||
return this.asError(this.intl.formatMessage({
|
||||
id: 'apps.error.parser.no_argument_pos_x',
|
||||
defaultMessage: 'Unable to identify argument.',
|
||||
|
|
@ -349,6 +349,13 @@ export class ParsedCommand {
|
|||
}));
|
||||
}
|
||||
|
||||
if (!this.field.name) {
|
||||
return this.asError(this.intl.formatMessage({
|
||||
id: 'apps.error.parser.missing_field_name',
|
||||
defaultMessage: 'Field name is missing.',
|
||||
}));
|
||||
}
|
||||
|
||||
if (autocompleteMode && c === '') {
|
||||
return this;
|
||||
}
|
||||
|
|
@ -595,6 +602,13 @@ export class ParsedCommand {
|
|||
}));
|
||||
}
|
||||
|
||||
if (!this.field.name) {
|
||||
return this.asError(this.intl.formatMessage({
|
||||
id: 'apps.error.parser.missing_field_name',
|
||||
defaultMessage: 'Field name is missing.',
|
||||
}));
|
||||
}
|
||||
|
||||
// special handling for optional BOOL values ('--boolflag true'
|
||||
// vs '--boolflag next-positional' vs '--boolflag
|
||||
// --next-flag...')
|
||||
|
|
@ -628,7 +642,14 @@ export class ParsedCommand {
|
|||
}));
|
||||
}
|
||||
|
||||
this.values![this.field.name] = [];
|
||||
if (!this.field.name) {
|
||||
return this.asError(this.intl.formatMessage({
|
||||
id: 'apps.error.parser.missing_field_name',
|
||||
defaultMessage: 'Field name is missing.',
|
||||
}));
|
||||
}
|
||||
|
||||
this.values![this.field.name] = [];
|
||||
switch (c) {
|
||||
case ' ':
|
||||
case '\t':
|
||||
|
|
@ -784,6 +805,13 @@ export class ParsedCommand {
|
|||
}));
|
||||
}
|
||||
|
||||
if (!this.field.name) {
|
||||
return this.asError(this.intl.formatMessage({
|
||||
id: 'apps.error.parser.missing_field_name',
|
||||
defaultMessage: 'Field name is missing.',
|
||||
}));
|
||||
}
|
||||
|
||||
if (autocompleteMode && c === '') {
|
||||
return this;
|
||||
}
|
||||
|
|
@ -961,7 +989,7 @@ export class AppCommandParser {
|
|||
}
|
||||
|
||||
await Promise.all(parsed.resolvedForm?.fields.map(async (f) => {
|
||||
if (!f.value) {
|
||||
if (!f.value || !f.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -975,6 +1003,9 @@ export class AppCommandParser {
|
|||
break;
|
||||
case AppFieldTypes.USER: {
|
||||
const userID = (f.value as AppSelectOption).value;
|
||||
if (!userID) {
|
||||
return;
|
||||
}
|
||||
let user: UserModel | UserProfile | undefined = await getUserById(this.database, userID);
|
||||
if (!user) {
|
||||
const res = await fetchUsersByIds(this.serverUrl, [userID]);
|
||||
|
|
@ -992,6 +1023,9 @@ export class AppCommandParser {
|
|||
}
|
||||
case AppFieldTypes.CHANNEL: {
|
||||
const channelID = (f.value as AppSelectOption).label;
|
||||
if (!channelID) {
|
||||
return;
|
||||
}
|
||||
let channel: ChannelModel | Channel | undefined = await getChannelById(this.database, channelID);
|
||||
if (!channel) {
|
||||
const res = await fetchChannelById(this.serverUrl, channelID);
|
||||
|
|
@ -1008,9 +1042,14 @@ export class AppCommandParser {
|
|||
break;
|
||||
}
|
||||
case AppFieldTypes.STATIC_SELECT:
|
||||
case AppFieldTypes.DYNAMIC_SELECT:
|
||||
parsed.values[f.name] = (f.value as AppSelectOption).value;
|
||||
case AppFieldTypes.DYNAMIC_SELECT: {
|
||||
const optionValue = (f.value as AppSelectOption).value;
|
||||
if (!optionValue) {
|
||||
return;
|
||||
}
|
||||
parsed.values[f.name] = optionValue;
|
||||
break;
|
||||
}
|
||||
case AppFieldTypes.MARKDOWN:
|
||||
|
||||
// Do nothing
|
||||
|
|
@ -1026,11 +1065,12 @@ export class AppCommandParser {
|
|||
|
||||
const bindings = this.getCommandBindings();
|
||||
for (const binding of bindings) {
|
||||
let base = binding.label;
|
||||
if (!base) {
|
||||
if (!binding.label) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let base = binding.label;
|
||||
|
||||
if (base[0] !== '/') {
|
||||
base = '/' + base;
|
||||
}
|
||||
|
|
@ -1100,7 +1140,7 @@ export class AppCommandParser {
|
|||
];
|
||||
const call = parsed.resolvedForm?.submit || parsed.binding?.form?.submit;
|
||||
const hasRequired = this.getMissingFields(parsed).length === 0;
|
||||
const hasValue = (parsed.state !== ParseState.EndValue || (parsed.field && parsed.values[parsed.field.name] !== undefined));
|
||||
const hasValue = (parsed.state !== ParseState.EndValue || (parsed.field && parsed.field.name && parsed.values[parsed.field.name] !== undefined));
|
||||
|
||||
if (executableStates.includes(parsed.state) && call && hasRequired && hasValue) {
|
||||
const execute = getExecuteSuggestion(parsed);
|
||||
|
|
@ -1181,7 +1221,11 @@ export class AppCommandParser {
|
|||
|
||||
const errors: {[key: string]: string} = {};
|
||||
await Promise.all(parsed.resolvedForm.fields.map(async (f) => {
|
||||
const fieldValue = values[f.name];
|
||||
const fieldName = f.name;
|
||||
if (!fieldName) {
|
||||
return;
|
||||
}
|
||||
const fieldValue = values[fieldName];
|
||||
if (!fieldValue) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1198,20 +1242,20 @@ export class AppCommandParser {
|
|||
const options: AppSelectOption[] = [];
|
||||
for (const value of commandValues) {
|
||||
if (options.find((o) => o.value === value)) {
|
||||
errors[f.name] = this.intl.formatMessage({
|
||||
errors[fieldName] = this.intl.formatMessage({
|
||||
id: 'apps.error.command.same_option',
|
||||
defaultMessage: 'Option repeated for field `{fieldName}`: `{option}`.',
|
||||
}, {
|
||||
fieldName: f.name,
|
||||
fieldName,
|
||||
option: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
values[f.name] = options;
|
||||
values[fieldName] = options;
|
||||
break;
|
||||
}
|
||||
|
||||
values[f.name] = {label: fieldValue, value: fieldValue};
|
||||
values[fieldName] = {label: fieldValue, value: fieldValue};
|
||||
break;
|
||||
case AppFieldTypes.STATIC_SELECT: {
|
||||
const getOption = (value: string) => {
|
||||
|
|
@ -1219,14 +1263,14 @@ export class AppCommandParser {
|
|||
};
|
||||
|
||||
const setOptionError = (value: string) => {
|
||||
errors[f.name] = this.intl.formatMessage({
|
||||
errors[fieldName] = this.intl.formatMessage({
|
||||
id: 'apps.error.command.unknown_option',
|
||||
defaultMessage: 'Unknown option for field `{fieldName}`: `{option}`.',
|
||||
}, {
|
||||
fieldName: f.name,
|
||||
fieldName,
|
||||
option: value,
|
||||
});
|
||||
values[f.name] = undefined;
|
||||
values[fieldName] = undefined;
|
||||
};
|
||||
|
||||
if (f.multiselect) {
|
||||
|
|
@ -1245,17 +1289,17 @@ export class AppCommandParser {
|
|||
return;
|
||||
}
|
||||
if (options.find((o) => o.value === option.value)) {
|
||||
errors[f.name] = this.intl.formatMessage({
|
||||
errors[fieldName] = this.intl.formatMessage({
|
||||
id: 'apps.error.command.same_option',
|
||||
defaultMessage: 'Option repeated for field `{fieldName}`: `{option}`.',
|
||||
}, {
|
||||
fieldName: f.name,
|
||||
fieldName,
|
||||
option: value,
|
||||
});
|
||||
}
|
||||
options.push(option);
|
||||
}
|
||||
values[f.name] = options;
|
||||
values[fieldName] = options;
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -1264,7 +1308,7 @@ export class AppCommandParser {
|
|||
setOptionError(fieldValue);
|
||||
return;
|
||||
}
|
||||
values[f.name] = option;
|
||||
values[fieldName] = option;
|
||||
break;
|
||||
}
|
||||
case AppFieldTypes.USER: {
|
||||
|
|
@ -1281,11 +1325,11 @@ export class AppCommandParser {
|
|||
};
|
||||
|
||||
const setUserError = (username: string) => {
|
||||
errors[f.name] = this.intl.formatMessage({
|
||||
errors[fieldName] = this.intl.formatMessage({
|
||||
id: 'apps.error.command.unknown_user',
|
||||
defaultMessage: 'Unknown user for field `{fieldName}`: `{option}`.',
|
||||
}, {
|
||||
fieldName: f.name,
|
||||
fieldName,
|
||||
option: username,
|
||||
});
|
||||
};
|
||||
|
|
@ -1312,22 +1356,22 @@ export class AppCommandParser {
|
|||
}
|
||||
|
||||
if (options.find((o) => o.value === user?.id)) {
|
||||
errors[f.name] = this.intl.formatMessage({
|
||||
errors[fieldName] = this.intl.formatMessage({
|
||||
id: 'apps.error.command.same_user',
|
||||
defaultMessage: 'User repeated for field `{fieldName}`: `{option}`.',
|
||||
}, {
|
||||
fieldName: f.name,
|
||||
fieldName,
|
||||
option: userName,
|
||||
});
|
||||
}
|
||||
options.push({label: user.username, value: user.id});
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
values[f.name] = options;
|
||||
values[fieldName] = options;
|
||||
break;
|
||||
}
|
||||
|
||||
let userName = values[f.name] as string;
|
||||
let userName = values[fieldName] as string;
|
||||
if (userName[0] === '@') {
|
||||
userName = userName.substr(1);
|
||||
}
|
||||
|
|
@ -1336,7 +1380,7 @@ export class AppCommandParser {
|
|||
setUserError(userName);
|
||||
return;
|
||||
}
|
||||
values[f.name] = {label: user.username, value: user.id};
|
||||
values[fieldName] = {label: user.username, value: user.id};
|
||||
break;
|
||||
}
|
||||
case AppFieldTypes.CHANNEL: {
|
||||
|
|
@ -1353,11 +1397,11 @@ export class AppCommandParser {
|
|||
};
|
||||
|
||||
const setChannelError = (channelName: string) => {
|
||||
errors[f.name] = this.intl.formatMessage({
|
||||
errors[fieldName] = this.intl.formatMessage({
|
||||
id: 'apps.error.command.unknown_channel',
|
||||
defaultMessage: 'Unknown channel for field `{fieldName}`: `{option}`.',
|
||||
}, {
|
||||
fieldName: f.name,
|
||||
fieldName,
|
||||
option: channelName,
|
||||
});
|
||||
};
|
||||
|
|
@ -1384,11 +1428,11 @@ export class AppCommandParser {
|
|||
}
|
||||
|
||||
if (options.find((o) => o.value === channel.id)) {
|
||||
errors[f.name] = this.intl.formatMessage({
|
||||
errors[fieldName] = this.intl.formatMessage({
|
||||
id: 'apps.error.command.same_channel',
|
||||
defaultMessage: 'Channel repeated for field `{fieldName}`: `{option}`.',
|
||||
}, {
|
||||
fieldName: f.name,
|
||||
fieldName,
|
||||
option: channelName,
|
||||
});
|
||||
}
|
||||
|
|
@ -1397,11 +1441,11 @@ export class AppCommandParser {
|
|||
options.push({label, value: channel.id});
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
values[f.name] = options;
|
||||
values[fieldName] = options;
|
||||
break;
|
||||
}
|
||||
|
||||
let channelName = values[f.name] as string;
|
||||
let channelName = values[fieldName] as string;
|
||||
if (channelName[0] === '~') {
|
||||
channelName = channelName.substr(1);
|
||||
}
|
||||
|
|
@ -1411,15 +1455,15 @@ export class AppCommandParser {
|
|||
return;
|
||||
}
|
||||
const label = 'display_name' in channel ? channel.display_name : channel.displayName;
|
||||
values[f.name] = {label, value: channel.id};
|
||||
values[fieldName] = {label, value: channel.id};
|
||||
break;
|
||||
}
|
||||
case AppFieldTypes.BOOL: {
|
||||
const strValue = values[f.name] as string;
|
||||
const strValue = values[fieldName] as string;
|
||||
if (strValue.toLowerCase() === 'true') {
|
||||
values[f.name] = true;
|
||||
values[fieldName] = true;
|
||||
} else {
|
||||
values[f.name] = false;
|
||||
values[fieldName] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1501,7 +1545,7 @@ export class AppCommandParser {
|
|||
// getAppContext collects post/channel/team info for performing calls
|
||||
private getAppContext = async (binding: AppBinding): Promise<AppContext> => {
|
||||
const context: AppContext = {
|
||||
app_id: binding.app_id,
|
||||
app_id: binding.app_id!, // At this point is safe to assume we have an app_id
|
||||
location: binding.location,
|
||||
root_id: this.rootPostID,
|
||||
};
|
||||
|
|
@ -1591,7 +1635,7 @@ export class AppCommandParser {
|
|||
const result: AutocompleteSuggestion[] = [];
|
||||
|
||||
bindings.forEach((b) => {
|
||||
if (b.label.toLowerCase().startsWith(parsed.incomplete.toLowerCase())) {
|
||||
if (b.label?.toLowerCase().startsWith(parsed.incomplete.toLowerCase())) {
|
||||
result.push({
|
||||
Complete: b.label,
|
||||
Suggestion: b.label,
|
||||
|
|
@ -1690,7 +1734,7 @@ export class AppCommandParser {
|
|||
const values = parsed.values || [];
|
||||
const fields = form.fields || [];
|
||||
for (const field of fields) {
|
||||
if (field.is_required && !values[field.name]) {
|
||||
if (field.is_required && field.name && !values[field.name]) { // fields without names shouldn't be considered
|
||||
missing.push(field);
|
||||
}
|
||||
}
|
||||
|
|
@ -1717,6 +1761,7 @@ export class AppCommandParser {
|
|||
const applicable = parsed.resolvedForm.fields.filter((field) => (
|
||||
field.label &&
|
||||
field.label.toLowerCase().startsWith(parsed.incomplete.toLowerCase()) &&
|
||||
field.name &&
|
||||
!parsed.values[field.name] &&
|
||||
!field.readonly &&
|
||||
field.type !== AppFieldTypes.MARKDOWN
|
||||
|
|
@ -1780,7 +1825,7 @@ export class AppCommandParser {
|
|||
// getStaticSelectSuggestions returns suggestions specified in the field's options property
|
||||
private getStaticSelectSuggestions = (parsed: ParsedCommand, delimiter?: string): AutocompleteSuggestion[] => {
|
||||
const f = parsed.field as AutocompleteStaticSelect;
|
||||
const opts = f.options?.filter((opt) => opt.label.toLowerCase().startsWith(parsed.incomplete.toLowerCase()));
|
||||
const opts = f.options?.filter((opt) => opt.label?.toLowerCase().startsWith(parsed.incomplete.toLowerCase()));
|
||||
if (!opts?.length) {
|
||||
return [{
|
||||
Complete: '',
|
||||
|
|
@ -1794,15 +1839,15 @@ export class AppCommandParser {
|
|||
}];
|
||||
}
|
||||
return opts.map((opt) => {
|
||||
let complete = opt.value;
|
||||
let complete = opt.value || '';
|
||||
if (delimiter) {
|
||||
complete = delimiter + complete + delimiter;
|
||||
} else if (isMultiword(opt.value)) {
|
||||
} else if (isMultiword(complete)) {
|
||||
complete = '`' + complete + '`';
|
||||
}
|
||||
return {
|
||||
Complete: complete,
|
||||
Suggestion: opt.label,
|
||||
Suggestion: opt.label || '',
|
||||
Hint: f.hint || '',
|
||||
Description: f.description || '',
|
||||
IconData: opt.icon_data || parsed.binding?.icon || '',
|
||||
|
|
@ -1883,16 +1928,16 @@ export class AppCommandParser {
|
|||
}
|
||||
|
||||
return items.map((s): AutocompleteSuggestion => {
|
||||
let complete = s.value;
|
||||
let complete = s.value || '';
|
||||
if (delimiter) {
|
||||
complete = delimiter + complete + delimiter;
|
||||
} else if (isMultiword(s.value)) {
|
||||
} else if (isMultiword(complete)) {
|
||||
complete = '`' + complete + '`';
|
||||
}
|
||||
return ({
|
||||
Complete: complete,
|
||||
Description: s.label || s.value,
|
||||
Suggestion: s.value,
|
||||
Description: s.label || s.value || '',
|
||||
Suggestion: s.value || '',
|
||||
Hint: '',
|
||||
IconData: s.icon_data || parsed.binding?.icon || '',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
import Autocomplete from '@components/autocomplete';
|
||||
import {ExtraKeyboard} from '@context/extra_keyboard';
|
||||
|
|
@ -51,6 +52,7 @@ function PostDraft({
|
|||
const [postInputTop, setPostInputTop] = useState(0);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const keyboardHeight = useKeyboardHeight();
|
||||
const kbHeight = Platform.OS === 'ios' ? keyboardHeight : 0; // useKeyboardHeight is already deducting the keyboard height on Android
|
||||
const headerHeight = useDefaultHeaderHeight();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
|
|
@ -60,9 +62,8 @@ function PostDraft({
|
|||
setCursorPosition(message.length);
|
||||
}, [channelId, rootId]);
|
||||
|
||||
const autocompletePosition = AUTOCOMPLETE_ADJUST + keyboardHeight + postInputTop;
|
||||
const autocompletePosition = AUTOCOMPLETE_ADJUST + kbHeight + postInputTop;
|
||||
const autocompleteAvailableSpace = containerHeight - autocompletePosition - (isChannelScreen ? headerHeight : 0);
|
||||
|
||||
const [animatedAutocompletePosition, animatedAutocompleteAvailableSpace] = useAutocompleteDefaultAnimatedValues(autocompletePosition, autocompleteAvailableSpace);
|
||||
|
||||
if (channelIsArchived || deactivatedChannel) {
|
||||
|
|
@ -122,7 +123,7 @@ function PostDraft({
|
|||
<>
|
||||
{draftHandler}
|
||||
{autoComplete}
|
||||
<ExtraKeyboard/>
|
||||
{Platform.OS !== 'android' && <ExtraKeyboard/>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const ButtonBinding = ({currentTeamId, binding, post, teamID, theme}: Props) =>
|
|||
pressed.current = true;
|
||||
|
||||
const context = createCallContext(
|
||||
binding.app_id,
|
||||
binding.app_id!,
|
||||
AppBindingLocations.IN_POST + binding.location,
|
||||
post.channelId,
|
||||
teamID || currentTeamId,
|
||||
|
|
@ -127,7 +127,7 @@ const ButtonBinding = ({currentTeamId, binding, post, teamID, theme}: Props) =>
|
|||
onPress={onPress}
|
||||
>
|
||||
<ButtonBindingText
|
||||
message={binding.label}
|
||||
message={binding.label || intl.formatMessage({id: 'apps.binding.default_button_name', defaultMessage: 'Submit'})}
|
||||
style={style.text}
|
||||
/>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const EmbeddedBinding = ({embed, location, post, theme}: Props) => {
|
|||
return (
|
||||
<>
|
||||
<View style={style.container}>
|
||||
{Boolean(embed.label) &&
|
||||
{embed.label &&
|
||||
<EmbedTitle
|
||||
channelId={post.channelId}
|
||||
location={location}
|
||||
|
|
@ -59,11 +59,11 @@ const EmbeddedBinding = ({embed, location, post, theme}: Props) => {
|
|||
value={embed.label}
|
||||
/>
|
||||
}
|
||||
{Boolean(embed.description) &&
|
||||
{embed.description &&
|
||||
<EmbedText
|
||||
channelId={post.channelId}
|
||||
location={location}
|
||||
value={embed.description!}
|
||||
value={embed.description}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,8 +61,8 @@ const MenuBinding = ({binding, currentTeamId, post, teamID}: Props) => {
|
|||
finish();
|
||||
}, [handleBindingSubmit, binding.bindings]);
|
||||
|
||||
const options = useMemo(() => binding.bindings?.map<PostActionOption>((b: AppBinding) => ({
|
||||
text: b.label,
|
||||
const options = useMemo(() => binding.bindings?.map<DialogOption>((b: AppBinding) => ({
|
||||
text: b.label || '',
|
||||
value: b.location || '',
|
||||
})), [binding.bindings]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import React, {useCallback, useMemo, useState} from 'react';
|
||||
|
||||
import {selectAttachmentMenuAction} from '@actions/remote/integrations';
|
||||
import AutocompleteSelector from '@components/autocomplete_selector';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {filterOptions} from '@utils/message_attachment';
|
||||
|
||||
type Props = {
|
||||
dataSource?: string;
|
||||
|
|
@ -18,12 +19,18 @@ type Props = {
|
|||
}
|
||||
|
||||
const ActionMenu = ({dataSource, defaultOption, disabled, id, name, options, postId}: Props) => {
|
||||
let isSelected: PostActionOption | undefined;
|
||||
const serverUrl = useServerUrl();
|
||||
if (defaultOption && options) {
|
||||
isSelected = options.find((option) => option.value === defaultOption);
|
||||
}
|
||||
const [selected, setSelected] = useState(isSelected?.value);
|
||||
|
||||
const filteredOptions = useMemo(() => {
|
||||
return filterOptions(options);
|
||||
}, [options]);
|
||||
|
||||
const [selected, setSelected] = useState(() => {
|
||||
if (defaultOption && options) {
|
||||
return options.find((option) => option.value === defaultOption)?.value;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const handleSelect = useCallback(async (selectedItem: SelectedDialogOption) => {
|
||||
if (!selectedItem || Array.isArray(selectedItem)) {
|
||||
|
|
@ -34,14 +41,14 @@ const ActionMenu = ({dataSource, defaultOption, disabled, id, name, options, pos
|
|||
if (result.data?.trigger_id) {
|
||||
setSelected(selectedItem.value);
|
||||
}
|
||||
}, []);
|
||||
}, [id, postId, serverUrl]);
|
||||
|
||||
return (
|
||||
<AutocompleteSelector
|
||||
placeholder={name}
|
||||
dataSource={dataSource}
|
||||
isMultiselect={false}
|
||||
options={options}
|
||||
options={filteredOptions}
|
||||
selected={selected}
|
||||
onSelected={handleSelect}
|
||||
disabled={disabled}
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export default function MessageAttachment({attachment, channelId, layoutWidth, l
|
|||
value={attachment.title}
|
||||
/>
|
||||
}
|
||||
{isValidUrl(attachment.thumb_url) &&
|
||||
{attachment.thumb_url && isValidUrl(attachment.thumb_url) &&
|
||||
<AttachmentThumbnail uri={attachment.thumb_url}/>
|
||||
}
|
||||
{Boolean(attachment.text) &&
|
||||
|
|
@ -114,7 +114,7 @@ export default function MessageAttachment({attachment, channelId, layoutWidth, l
|
|||
theme={theme}
|
||||
/>
|
||||
}
|
||||
{Boolean(attachment.fields?.length) &&
|
||||
{attachment.fields && attachment.fields?.length &&
|
||||
<AttachmentFields
|
||||
baseTextStyle={style.message}
|
||||
blockStyles={blockStyles}
|
||||
|
|
@ -126,21 +126,21 @@ export default function MessageAttachment({attachment, channelId, layoutWidth, l
|
|||
theme={theme}
|
||||
/>
|
||||
}
|
||||
{Boolean(attachment.footer) &&
|
||||
{attachment.footer &&
|
||||
<AttachmentFooter
|
||||
icon={attachment.footer_icon}
|
||||
text={attachment.footer}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
{Boolean(attachment.actions?.length) &&
|
||||
{attachment.actions && attachment.actions.length &&
|
||||
<AttachmentActions
|
||||
actions={attachment.actions!}
|
||||
postId={postId}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
{Boolean(metadata?.images?.[attachment.image_url]) &&
|
||||
{attachment.image_url && Boolean(metadata?.images?.[attachment.image_url]) &&
|
||||
<AttachmentImage
|
||||
imageUrl={attachment.image_url}
|
||||
imageMetadata={metadata!.images![attachment.image_url]!}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
|
||||
type Props = {
|
||||
label: string;
|
||||
options?: PostActionOption[];
|
||||
options?: DialogOption[];
|
||||
onChange: (value: string) => void;
|
||||
helpText?: string;
|
||||
errorText?: string;
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ export const ExtraKeyboardProvider = (({children}: {children: React.ReactElement
|
|||
const [isTextInputFocused, setIsTextInputFocused] = useState(false);
|
||||
|
||||
const showExtraKeyboard = useCallback((newComponent: React.ReactElement|null) => {
|
||||
// Do not use ExtraKeyboard on Android versions below 11
|
||||
if (Platform.OS === 'android' && Platform.Version < 30) {
|
||||
return;
|
||||
}
|
||||
setExtraKeyboardVisible(true);
|
||||
setComponent(newComponent);
|
||||
if (Keyboard.isVisible()) {
|
||||
|
|
@ -62,6 +66,10 @@ export const ExtraKeyboardProvider = (({children}: {children: React.ReactElement
|
|||
}, []);
|
||||
|
||||
const hideExtraKeyboard = useCallback(() => {
|
||||
// Do not use ExtraKeyboard on Android versions below 11
|
||||
if (Platform.OS === 'android' && Platform.Version < 30) {
|
||||
return;
|
||||
}
|
||||
setExtraKeyboardVisible(false);
|
||||
setComponent(null);
|
||||
if (Keyboard.isVisible()) {
|
||||
|
|
@ -70,6 +78,11 @@ export const ExtraKeyboardProvider = (({children}: {children: React.ReactElement
|
|||
}, []);
|
||||
|
||||
const registerTextInputFocus = useCallback(() => {
|
||||
// Do not use ExtraKeyboard on Android versions below 11
|
||||
if (Platform.OS === 'android' && Platform.Version < 30) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the extra keyboard is opened if we don't do this
|
||||
// we get a glitch in the UI that will animate the extra keyboard down
|
||||
// and immediately bring the keyboard, by doing this
|
||||
|
|
@ -81,11 +94,21 @@ export const ExtraKeyboardProvider = (({children}: {children: React.ReactElement
|
|||
}, []);
|
||||
|
||||
const registerTextInputBlur = useCallback(() => {
|
||||
// Do not use ExtraKeyboard on Android versions below 11
|
||||
if (Platform.OS === 'android' && Platform.Version < 30) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsTextInputFocused(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const keyboardHideListener = Keyboard.addListener('keyboardDidHide', () => {
|
||||
// Do not use ExtraKeyboard on Android versions below 11
|
||||
if (Platform.OS === 'android' && Platform.Version < 30) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTextInputFocused) {
|
||||
setExtraKeyboardVisible(false);
|
||||
}
|
||||
|
|
@ -142,7 +165,7 @@ export const useHideExtraKeyboardIfNeeded = (callback: (...args: any) => void, d
|
|||
}), [keyboardContext, ...dependencies]);
|
||||
};
|
||||
|
||||
export const ExtraKeyboard = () => {
|
||||
const ExtraKeyboardComponent = () => {
|
||||
const keyb = useAnimatedKeyboard({isStatusBarTranslucentAndroid: true});
|
||||
const defaultKeyboardHeight = Platform.select({ios: 291, default: 240});
|
||||
const context = useExtraKeyboardContext();
|
||||
|
|
@ -181,3 +204,14 @@ export const ExtraKeyboard = () => {
|
|||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
// Do not use ExtraKeyboard on Android versions below 11
|
||||
export const ExtraKeyboard = () => {
|
||||
if (Platform.OS === 'android' && Platform.Version < 30) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ExtraKeyboardComponent/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export const useAppBinding = (context: UseAppBindingContext, config: UseAppBindi
|
|||
|
||||
return useCallback(async (binding: AppBinding) => {
|
||||
const callContext = createCallContext(
|
||||
binding.app_id,
|
||||
binding.app_id!,
|
||||
binding.location,
|
||||
context.channel_id,
|
||||
context.team_id,
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ export const useAutocompleteDefaultAnimatedValues = (position: number, available
|
|||
|
||||
useEffect(() => {
|
||||
animatedPosition.value = position;
|
||||
}, [position]);
|
||||
}, [position, animatedPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
animatedAvailableSpace.value = availableSpace;
|
||||
}, [availableSpace]);
|
||||
}, [availableSpace, animatedAvailableSpace]);
|
||||
|
||||
return [animatedPosition, animatedAvailableSpace];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -61,10 +61,19 @@ export function useKeyboardHeightWithDuration() {
|
|||
|
||||
useEffect(() => {
|
||||
const show = Keyboard.addListener(Platform.select({ios: 'keyboardWillShow', default: 'keyboardDidShow'}), async (event) => {
|
||||
// Do not use set the height on Android versions below 11
|
||||
if (Platform.OS === 'android' && Platform.Version < 30) {
|
||||
return;
|
||||
}
|
||||
setKeyboardHeight({height: event.endCoordinates.height, duration: event.duration});
|
||||
});
|
||||
|
||||
const hide = Keyboard.addListener(Platform.select({ios: 'keyboardWillHide', default: 'keyboardDidHide'}), (event) => {
|
||||
// Do not use set the height on Android versions below 11
|
||||
if (Platform.OS === 'android' && Platform.Version < 30) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (updateTimeout.current != null) {
|
||||
clearTimeout(updateTimeout.current);
|
||||
updateTimeout.current = null;
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ describe('SessionManager', () => {
|
|||
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(logout).toHaveBeenCalledWith(mockServerUrl, false, false, true);
|
||||
expect(logout).toHaveBeenCalledWith(mockServerUrl, undefined, {skipEvents: true, skipServerLogout: true});
|
||||
expect(relaunchApp).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -187,7 +187,11 @@ export class SessionManager {
|
|||
|
||||
private onSessionExpired = async (serverUrl: string) => {
|
||||
this.terminatingSessionUrl.add(serverUrl);
|
||||
await logout(serverUrl, false, false, true);
|
||||
|
||||
// logout is not doing anything in this scenario, but we keep it
|
||||
// to keep the same flow as other logout scenarios.
|
||||
await logout(serverUrl, undefined, {skipServerLogout: true, skipEvents: true});
|
||||
|
||||
await this.terminateSession(serverUrl, false);
|
||||
|
||||
const activeServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
|
|
|
|||
|
|
@ -103,6 +103,9 @@ function valuesReducer(state: AppFormValues, action: ValuesAction) {
|
|||
function initValues(fields?: AppField[]) {
|
||||
const values: AppFormValues = {};
|
||||
fields?.forEach((e) => {
|
||||
if (!e.name) {
|
||||
return;
|
||||
}
|
||||
if (e.type === 'bool') {
|
||||
values[e.name] = (e.value === true || String(e.value).toLowerCase() === 'true');
|
||||
} else if (e.value) {
|
||||
|
|
@ -327,7 +330,7 @@ function AppsFormComponent({
|
|||
|
||||
const performLookup = useCallback(async (name: string, userInput: string): Promise<AppSelectOption[]> => {
|
||||
const field = form.fields?.find((f) => f.name === name);
|
||||
if (!field) {
|
||||
if (!field?.name) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -409,6 +412,9 @@ function AppsFormComponent({
|
|||
/>
|
||||
}
|
||||
{form.fields && form.fields.filter((f) => f.name !== form.submit_buttons).map((field) => {
|
||||
if (!field.name) {
|
||||
return null;
|
||||
}
|
||||
const value = secureGetFromRecord(values, field.name);
|
||||
if (!value) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ const dialogOptionToAppSelectOption = (option: DialogOption): AppSelectOption =>
|
|||
});
|
||||
|
||||
const appSelectOptionToDialogOption = (option: AppSelectOption): DialogOption => ({
|
||||
text: option.label,
|
||||
value: option.value,
|
||||
text: option.label || '',
|
||||
value: option.value || '',
|
||||
});
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
|
|
@ -100,6 +100,9 @@ function AppsFormField({
|
|||
}, [onChange, field, name]);
|
||||
|
||||
const getDynamicOptions = useCallback(async (userInput = ''): Promise<DialogOption[]> => {
|
||||
if (!field.name) {
|
||||
return [];
|
||||
}
|
||||
const options = await performLookup(field.name, userInput);
|
||||
return options.map(appSelectOptionToDialogOption);
|
||||
}, [performLookup, field]);
|
||||
|
|
@ -126,7 +129,7 @@ function AppsFormField({
|
|||
}, [field, value]);
|
||||
|
||||
const selectedValue = useMemo(() => {
|
||||
if (!value || !SelectableAppFieldTypes.includes(field.type)) {
|
||||
if (!value || !SelectableAppFieldTypes.includes(field.type || '')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +138,7 @@ function AppsFormField({
|
|||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v) => v.value);
|
||||
return value.map((v) => v.value || '');
|
||||
}
|
||||
|
||||
return value as string;
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ const ChannelInfoAppBindings = ({channelId, teamId, dismissChannelInfo, serverUr
|
|||
|
||||
const options = bindings.map((binding) => (
|
||||
<BindingOptionItem
|
||||
key={binding.app_id + binding.location}
|
||||
key={(binding.app_id || '') + (binding.location || '')}
|
||||
binding={binding}
|
||||
onPress={onPress}
|
||||
/>
|
||||
|
|
@ -65,7 +65,7 @@ const BindingOptionItem = ({binding, onPress}: {binding: AppBinding; onPress: (b
|
|||
|
||||
return (
|
||||
<OptionItem
|
||||
label={binding.label}
|
||||
label={binding.label || ''}
|
||||
icon={binding.icon}
|
||||
action={handlePress}
|
||||
type='default'
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const LogOut = () => {
|
|||
|
||||
const onLogout = useCallback(preventDoubleTap(() => {
|
||||
Navigation.updateProps(Screens.HOME, {extra: undefined});
|
||||
alertServerLogout(serverDisplayName, () => logout(serverUrl), intl);
|
||||
alertServerLogout(serverDisplayName, () => logout(serverUrl, intl), intl);
|
||||
}), [serverDisplayName, serverUrl, intl]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -169,8 +169,8 @@ const ChannelListHeader = ({
|
|||
}, [pushProxyStatus, intl]);
|
||||
|
||||
const onLogoutPress = useCallback(() => {
|
||||
alertServerLogout(serverDisplayName, () => logout(serverUrl), intl);
|
||||
}, []);
|
||||
alertServerLogout(serverDisplayName, () => logout(serverUrl, intl), intl);
|
||||
}, [intl, serverDisplayName, serverUrl]);
|
||||
|
||||
let header;
|
||||
if (displayName) {
|
||||
|
|
|
|||
|
|
@ -181,21 +181,21 @@ const ServerItem = ({
|
|||
|
||||
const logoutServer = useCallback(async () => {
|
||||
Navigation.updateProps(Screens.HOME, {extra: undefined});
|
||||
await logout(server.url);
|
||||
await logout(server.url, intl);
|
||||
|
||||
if (isActive) {
|
||||
dismissBottomSheet();
|
||||
} else {
|
||||
DeviceEventEmitter.emit(Events.SWIPEABLE, '');
|
||||
}
|
||||
}, [isActive, server.url]);
|
||||
}, [intl, isActive, server.url]);
|
||||
|
||||
const removeServer = useCallback(async () => {
|
||||
const skipLogoutFromServer = server.lastActiveAt === 0;
|
||||
const skipServerLogout = server.lastActiveAt === 0;
|
||||
await dismissBottomSheet();
|
||||
Navigation.updateProps(Screens.HOME, {extra: undefined});
|
||||
await logout(server.url, skipLogoutFromServer, true);
|
||||
}, [server.lastActiveAt, server.url]);
|
||||
await logout(server.url, intl, {skipServerLogout, removeServer: true});
|
||||
}, [intl, server.lastActiveAt, server.url]);
|
||||
|
||||
const startTutorial = () => {
|
||||
viewRef.current?.measureInWindow((x, y, w, h) => {
|
||||
|
|
|
|||
|
|
@ -162,19 +162,19 @@ const RecentMentionsScreen = ({appsEnabled, customEmojiNames, mentions, currentT
|
|||
return (
|
||||
<Freeze freeze={!isFocused}>
|
||||
<ExtraKeyboardProvider>
|
||||
<NavigationHeader
|
||||
isLargeTitle={true}
|
||||
showBackButton={false}
|
||||
subtitle={subtitle}
|
||||
title={title}
|
||||
hasSearch={false}
|
||||
scrollValue={scrollValue}
|
||||
/>
|
||||
<SafeAreaView
|
||||
style={styles.flex}
|
||||
edges={EDGES}
|
||||
testID='recent_mentions.screen'
|
||||
>
|
||||
<NavigationHeader
|
||||
isLargeTitle={true}
|
||||
showBackButton={false}
|
||||
subtitle={subtitle}
|
||||
title={title}
|
||||
hasSearch={false}
|
||||
scrollValue={scrollValue}
|
||||
/>
|
||||
<Animated.View style={[styles.flex, animated]}>
|
||||
<Animated.View style={top}>
|
||||
<RoundedHeaderContext/>
|
||||
|
|
|
|||
|
|
@ -164,19 +164,19 @@ function SavedMessages({appsEnabled, posts, currentTimezone, customEmojiNames}:
|
|||
return (
|
||||
<Freeze freeze={!isFocused}>
|
||||
<ExtraKeyboardProvider>
|
||||
<NavigationHeader
|
||||
isLargeTitle={true}
|
||||
showBackButton={false}
|
||||
subtitle={subtitle}
|
||||
title={title}
|
||||
hasSearch={false}
|
||||
scrollValue={scrollValue}
|
||||
/>
|
||||
<SafeAreaView
|
||||
edges={edges}
|
||||
style={styles.flex}
|
||||
testID='saved_messages.screen'
|
||||
>
|
||||
<NavigationHeader
|
||||
isLargeTitle={true}
|
||||
showBackButton={false}
|
||||
subtitle={subtitle}
|
||||
title={title}
|
||||
hasSearch={false}
|
||||
scrollValue={scrollValue}
|
||||
/>
|
||||
<Animated.View style={[styles.flex, animated]}>
|
||||
<Animated.View style={top}>
|
||||
<RoundedHeaderContext/>
|
||||
|
|
|
|||
168
app/screens/home/search/search.test.tsx
Normal file
168
app/screens/home/search/search.test.tsx
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fireEvent, waitFor} from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
|
||||
import {addSearchToTeamSearchHistory} from '@actions/local/team';
|
||||
import {searchPosts, searchFiles} from '@actions/remote/search';
|
||||
import {bottomSheet} from '@screens/navigation';
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
|
||||
import SearchScreen from './search';
|
||||
|
||||
import type {TeamModel} from '@database/models/server';
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
|
||||
// Some subcomponents require react-native-camera-roll, which is not available in the test environment
|
||||
jest.mock('@react-native-camera-roll/camera-roll', () => ({}));
|
||||
|
||||
jest.mock('@react-navigation/native', () => ({
|
||||
...jest.requireActual('@react-navigation/native'),
|
||||
useNavigation: () => ({
|
||||
getState: () => ({
|
||||
index: 0,
|
||||
routes: [{params: {searchTerm: ''}}],
|
||||
}),
|
||||
}),
|
||||
useIsFocused: () => true,
|
||||
}));
|
||||
|
||||
jest.mock('@actions/local/post', () => ({
|
||||
getPosts: jest.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
jest.mock('@actions/local/team', () => ({
|
||||
addSearchToTeamSearchHistory: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@actions/remote/search', () => ({
|
||||
searchPosts: jest.fn().mockResolvedValue({order: [], matches: {}}),
|
||||
searchFiles: jest.fn().mockResolvedValue({files: [], channels: []}),
|
||||
}));
|
||||
|
||||
jest.mock('@mattermost/hardware-keyboard', () => ({
|
||||
useHardwareKeyboardEvents: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@screens/navigation', () => ({
|
||||
bottomSheet: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('SearchScreen', () => {
|
||||
const baseProps = {
|
||||
teamId: 'team1',
|
||||
teams: [
|
||||
{id: 'team1', displayName: 'Team 1'},
|
||||
{id: 'team2', displayName: 'Team 2'},
|
||||
] as TeamModel[],
|
||||
crossTeamSearchEnabled: true,
|
||||
};
|
||||
|
||||
let database: Database;
|
||||
beforeAll(async () => {
|
||||
const server = await TestHelper.setupServerDatabase();
|
||||
database = server.database;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders search screen correctly', () => {
|
||||
const {getByTestId, getByText, getByPlaceholderText} = renderWithEverything(
|
||||
<SearchScreen {...baseProps}/>,
|
||||
{database},
|
||||
);
|
||||
expect(getByTestId('search_messages.screen')).toBeTruthy();
|
||||
|
||||
// The page title
|
||||
expect(getByText('Search')).toBeTruthy();
|
||||
|
||||
// The search input with the expected placeholder
|
||||
expect(getByPlaceholderText('Search messages & files')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('handles search input changes', () => {
|
||||
const {getByTestId} = renderWithEverything(
|
||||
<SearchScreen {...baseProps}/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
const searchInput = getByTestId('navigation.header.search_bar.search.input');
|
||||
fireEvent.changeText(searchInput, 'test search');
|
||||
expect(searchInput.props.value).toBe('test search');
|
||||
});
|
||||
|
||||
it('performs search when submitting', async () => {
|
||||
const {getByTestId} = renderWithEverything(
|
||||
<SearchScreen {...baseProps}/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
const searchInput = getByTestId('navigation.header.search_bar.search.input');
|
||||
fireEvent.changeText(searchInput, 'test search');
|
||||
fireEvent(searchInput, 'submitEditing');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(searchPosts).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'team1',
|
||||
expect.objectContaining({terms: 'test search'}),
|
||||
);
|
||||
expect(searchFiles).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'team1',
|
||||
expect.objectContaining({terms: 'test search'}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles team changes', async () => {
|
||||
const {getByTestId} = renderWithEverything(
|
||||
<SearchScreen {...baseProps}/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
const teamPicker = getByTestId('team_picker.button');
|
||||
fireEvent.press(teamPicker);
|
||||
|
||||
expect(teamPicker).toBeTruthy();
|
||||
expect(bottomSheet).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clears search when clear button is pressed', async () => {
|
||||
const {getByTestId} = renderWithEverything(
|
||||
<SearchScreen {...baseProps}/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
const searchInput = getByTestId('navigation.header.search_bar.search.input');
|
||||
fireEvent.changeText(searchInput, 'test search');
|
||||
|
||||
const clearButton = getByTestId('navigation.header.search_bar.search.clear.button');
|
||||
fireEvent.press(clearButton);
|
||||
|
||||
expect(searchInput.props.value).toBe('');
|
||||
});
|
||||
|
||||
it('adds search to team history when searching in a specific team', async () => {
|
||||
const {getByTestId} = renderWithEverything(
|
||||
<SearchScreen {...baseProps}/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
const searchInput = getByTestId('navigation.header.search_bar.search.input');
|
||||
fireEvent.changeText(searchInput, 'test search');
|
||||
fireEvent(searchInput, 'submitEditing');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addSearchToTeamSearchHistory).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'team1',
|
||||
'test search',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -79,6 +79,8 @@ const getSearchParams = (terms: string, filterValue?: FileFilter) => {
|
|||
|
||||
const searchScreenIndex = 1;
|
||||
|
||||
const CHANNEL_AND_USER_FILTERS_REGEX = /(?:from|channel|in):\s?[^\s\n]+/gi;
|
||||
|
||||
const SearchScreen = ({teamId, teams, crossTeamSearchEnabled}: Props) => {
|
||||
const nav = useNavigation();
|
||||
const isFocused = useIsFocused();
|
||||
|
|
@ -238,9 +240,20 @@ const SearchScreen = ({teamId, teams, crossTeamSearchEnabled}: Props) => {
|
|||
setResultsLoading(false);
|
||||
}, [lastSearchedValue, searchTeamId, serverUrl]);
|
||||
|
||||
const removeChannelAndUserFiltersFromString = (str: string) => {
|
||||
return str.replace(CHANNEL_AND_USER_FILTERS_REGEX, '').trim();
|
||||
};
|
||||
|
||||
const updateSearchTeamId = useCallback((newTeamId: string) => {
|
||||
setSearchTeamId(newTeamId);
|
||||
setSearchValue(removeChannelAndUserFiltersFromString(searchValue));
|
||||
}, [searchValue]);
|
||||
|
||||
const handleResultsTeamChange = useCallback((newTeamId: string) => {
|
||||
setSearchTeamId(newTeamId);
|
||||
handleSearch(newTeamId, lastSearchedValue);
|
||||
const cleanedSearchValue = removeChannelAndUserFiltersFromString(lastSearchedValue);
|
||||
setSearchValue(cleanedSearchValue);
|
||||
handleSearch(newTeamId, cleanedSearchValue);
|
||||
}, [lastSearchedValue, handleSearch]);
|
||||
|
||||
const initialContainerStyle: AnimatedStyle<ViewStyle> = useMemo(() => {
|
||||
|
|
@ -265,7 +278,7 @@ const SearchScreen = ({teamId, teams, crossTeamSearchEnabled}: Props) => {
|
|||
setRecentValue={handleRecentSearch}
|
||||
searchRef={searchRef}
|
||||
setSearchValue={handleModifierTextChange}
|
||||
setTeamId={setSearchTeamId}
|
||||
setTeamId={updateSearchTeamId}
|
||||
teamId={searchTeamId}
|
||||
teams={teams}
|
||||
/>
|
||||
|
|
@ -357,31 +370,31 @@ const SearchScreen = ({teamId, teams, crossTeamSearchEnabled}: Props) => {
|
|||
|
||||
return (
|
||||
<Freeze freeze={!isFocused}>
|
||||
<NavigationHeader
|
||||
isLargeTitle={true}
|
||||
showBackButton={false}
|
||||
title={intl.formatMessage({id: 'screen.search.title', defaultMessage: 'Search'})}
|
||||
hasSearch={true}
|
||||
scrollValue={scrollValue}
|
||||
lockValue={lockValue}
|
||||
hideHeader={hideHeader}
|
||||
onChangeText={handleTextChange}
|
||||
onSubmitEditing={onSubmit}
|
||||
blurOnSubmit={true}
|
||||
placeholder={intl.formatMessage({id: 'screen.search.placeholder', defaultMessage: 'Search messages & files'})}
|
||||
onBlur={onBlur}
|
||||
onClear={handleClearSearch}
|
||||
onCancel={handleCancelSearch}
|
||||
onFocus={onFocus}
|
||||
defaultValue={searchValue}
|
||||
ref={searchRef}
|
||||
/>
|
||||
<SafeAreaView
|
||||
style={styles.flex}
|
||||
edges={EDGES}
|
||||
onLayout={onLayout}
|
||||
testID='search_messages.screen'
|
||||
>
|
||||
<NavigationHeader
|
||||
isLargeTitle={true}
|
||||
showBackButton={false}
|
||||
title={intl.formatMessage({id: 'screen.search.title', defaultMessage: 'Search'})}
|
||||
hasSearch={true}
|
||||
scrollValue={scrollValue}
|
||||
lockValue={lockValue}
|
||||
hideHeader={hideHeader}
|
||||
onChangeText={handleTextChange}
|
||||
onSubmitEditing={onSubmit}
|
||||
blurOnSubmit={true}
|
||||
placeholder={intl.formatMessage({id: 'screen.search.placeholder', defaultMessage: 'Search messages & files'})}
|
||||
onBlur={onBlur}
|
||||
onClear={handleClearSearch}
|
||||
onCancel={handleCancelSearch}
|
||||
onFocus={onFocus}
|
||||
defaultValue={searchValue}
|
||||
ref={searchRef}
|
||||
/>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.flex}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
popTopScreen, setButtons,
|
||||
} from '@screens/navigation';
|
||||
import {filterChannelsMatchingTerm} from '@utils/channel';
|
||||
import {filterOptions} from '@utils/message_attachment';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {secureGetFromRecord} from '@utils/types';
|
||||
import {typography} from '@utils/typography';
|
||||
|
|
@ -189,6 +190,10 @@ function IntegrationSelector(
|
|||
const page = useRef<number>(-1);
|
||||
const next = useRef<boolean>(VALID_DATASOURCES.includes(dataSource));
|
||||
|
||||
const filteredOptions = useMemo(() => {
|
||||
return filterOptions(options) || [];
|
||||
}, [options]);
|
||||
|
||||
// Callbacks
|
||||
const clearSearch = useCallback(() => {
|
||||
setTerm('');
|
||||
|
|
@ -275,8 +280,8 @@ function IntegrationSelector(
|
|||
}, [getChannels, dataSource]);
|
||||
|
||||
const searchDynamicOptions = useCallback(async (searchTerm = '') => {
|
||||
if (options && options !== integrationData && !searchTerm) {
|
||||
setIntegrationData(options);
|
||||
if (filteredOptions && filteredOptions !== integrationData && !searchTerm) {
|
||||
setIntegrationData(filteredOptions);
|
||||
}
|
||||
|
||||
if (!getDynamicOptions) {
|
||||
|
|
@ -291,7 +296,7 @@ function IntegrationSelector(
|
|||
} else {
|
||||
setIntegrationData(searchData);
|
||||
}
|
||||
}, [options, getDynamicOptions, integrationData]);
|
||||
}, [filteredOptions, getDynamicOptions, integrationData]);
|
||||
|
||||
const handleSelectProfile = useCallback((user: UserProfile): void => {
|
||||
if (!isMultiselect) {
|
||||
|
|
@ -300,7 +305,7 @@ function IntegrationSelector(
|
|||
}
|
||||
|
||||
setSelectedIds((current) => handleIdSelection(dataSource, current, user));
|
||||
}, [isMultiselect, handleIdSelection, handleSelect, close, dataSource]);
|
||||
}, [isMultiselect, handleSelect, dataSource]);
|
||||
|
||||
const onHandleMultiselectSubmit = useCallback(() => {
|
||||
if (dataSource === ViewConstants.DATA_SOURCE_USERS) {
|
||||
|
|
@ -311,7 +316,7 @@ function IntegrationSelector(
|
|||
handleSelect(Object.values(multiselectSelected));
|
||||
}
|
||||
close();
|
||||
}, [multiselectSelected, selectedIds, handleSelect]);
|
||||
}, [dataSource, handleSelect, selectedIds, multiselectSelected]);
|
||||
|
||||
const onSearch = useCallback((text: string) => {
|
||||
if (!text) {
|
||||
|
|
@ -347,7 +352,7 @@ function IntegrationSelector(
|
|||
|
||||
setLoading(false);
|
||||
}, General.SEARCH_TIMEOUT_MILLISECONDS);
|
||||
}, [dataSource, integrationData, currentTeamId]);
|
||||
}, [clearSearch, dataSource, integrationData, serverUrl, currentTeamId, searchDynamicOptions]);
|
||||
|
||||
// Effects
|
||||
useNavButtonPressed(SUBMIT_BUTTON_ID, componentId, onHandleMultiselectSubmit, [onHandleMultiselectSubmit]);
|
||||
|
|
@ -400,7 +405,7 @@ function IntegrationSelector(
|
|||
|
||||
if (isMultiselect && Array.isArray(selected) && !([ViewConstants.DATA_SOURCE_USERS, ViewConstants.DATA_SOURCE_CHANNELS].includes(dataSource))) {
|
||||
for (const value of selected) {
|
||||
const option = options?.find((opt) => opt.value === value);
|
||||
const option = filteredOptions?.find((opt) => opt.value === value);
|
||||
if (option) {
|
||||
multiselectItems[value] = option;
|
||||
}
|
||||
|
|
@ -440,7 +445,7 @@ function IntegrationSelector(
|
|||
/>
|
||||
</View>
|
||||
);
|
||||
}, [style, dataSource, loading, intl]);
|
||||
}, [style, dataSource, loading]);
|
||||
|
||||
const renderNoResults = useCallback((): JSX.Element | null => {
|
||||
if (loading || page.current === -1) {
|
||||
|
|
@ -518,7 +523,7 @@ function IntegrationSelector(
|
|||
<View style={style.separator}/>
|
||||
</>
|
||||
);
|
||||
}, [multiselectSelected, selectedIds, style, theme]);
|
||||
}, [dataSource, handleRemoveOption, multiselectSelected, selectedIds, style.separator, theme]);
|
||||
|
||||
const userFetchFunction = useCallback(async (userFetchPage: number) => {
|
||||
const result = await fetchProfiles(serverUrl, userFetchPage, General.PROFILE_CHUNK_SIZE);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
|
||||
import AutocompleteSelector from '@components/autocomplete_selector';
|
||||
import BoolSetting from '@components/settings/bool_setting';
|
||||
import RadioSetting from '@components/settings/radio_setting';
|
||||
import TextSetting from '@components/settings/text_setting';
|
||||
import {selectKeyboardType as selectKB} from '@utils/integrations';
|
||||
import {filterOptions} from '@utils/message_attachment';
|
||||
|
||||
import type {KeyboardTypeOptions} from 'react-native';
|
||||
|
||||
|
|
@ -86,6 +87,10 @@ function DialogElement({
|
|||
onChange(name, newValue.value);
|
||||
}, [name, onChange]);
|
||||
|
||||
const filteredOptions = useMemo(() => {
|
||||
return filterOptions(options);
|
||||
}, [options]);
|
||||
|
||||
switch (type) {
|
||||
case 'text':
|
||||
case 'textarea':
|
||||
|
|
@ -111,7 +116,7 @@ function DialogElement({
|
|||
<AutocompleteSelector
|
||||
label={displayName}
|
||||
dataSource={dataSource}
|
||||
options={options}
|
||||
options={filteredOptions}
|
||||
optional={optional}
|
||||
onSelected={handleSelect}
|
||||
helpText={helpText}
|
||||
|
|
@ -129,7 +134,7 @@ function DialogElement({
|
|||
label={displayName}
|
||||
helpText={helpText}
|
||||
errorText={errorText}
|
||||
options={options}
|
||||
options={filteredOptions}
|
||||
onChange={handleChange}
|
||||
testID={testID}
|
||||
value={getStringValue(value)}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
|
||||
import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Keyboard, ScrollView} from 'react-native';
|
||||
import {Keyboard} from 'react-native';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
import {type ImageResource, Navigation} from 'react-native-navigation';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
|
|
@ -106,7 +107,7 @@ function InteractiveDialog({
|
|||
const serverUrl = useServerUrl();
|
||||
const intl = useIntl();
|
||||
|
||||
const scrollView = useRef<ScrollView>(null);
|
||||
const scrollView = useRef<KeyboardAwareScrollView>(null);
|
||||
|
||||
const onChange = useCallback((name: string, value: string | number | boolean) => {
|
||||
dispatchValues({name, value});
|
||||
|
|
@ -185,7 +186,7 @@ function InteractiveDialog({
|
|||
if (data.error) {
|
||||
hasErrors = true;
|
||||
setError(data.error);
|
||||
scrollView.current?.scrollTo({x: 0, y: 0});
|
||||
scrollView.current?.scrollToPosition(0, 0, true);
|
||||
} else {
|
||||
setError('');
|
||||
}
|
||||
|
|
@ -234,9 +235,19 @@ function InteractiveDialog({
|
|||
testID='interactive_dialog.screen'
|
||||
style={style.container}
|
||||
>
|
||||
<ScrollView
|
||||
<KeyboardAwareScrollView
|
||||
ref={scrollView}
|
||||
bounces={false}
|
||||
style={style.scrollView}
|
||||
enableAutomaticScroll={true}
|
||||
enableOnAndroid={true}
|
||||
noPaddingBottomOnAndroid={true}
|
||||
scrollToOverflowEnabled={true}
|
||||
enableResetScrollToCoords={true}
|
||||
extraScrollHeight={0}
|
||||
extraHeight={0}
|
||||
keyboardDismissMode='interactive'
|
||||
keyboardShouldPersistTaps='handled'
|
||||
>
|
||||
{Boolean(error) && (
|
||||
<ErrorText
|
||||
|
|
@ -246,9 +257,9 @@ function InteractiveDialog({
|
|||
/>
|
||||
)}
|
||||
{Boolean(introductionText) &&
|
||||
<DialogIntroductionText
|
||||
value={introductionText}
|
||||
/>
|
||||
<DialogIntroductionText
|
||||
value={introductionText}
|
||||
/>
|
||||
}
|
||||
{Boolean(elements) && elements.map((e) => {
|
||||
const value = secureGetFromRecord(values, e.name);
|
||||
|
|
@ -271,7 +282,7 @@ function InteractiveDialog({
|
|||
/>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</KeyboardAwareScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,14 @@
|
|||
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import RNUtils from '@mattermost/rnutils';
|
||||
import merge from 'deepmerge';
|
||||
import {Appearance, DeviceEventEmitter, StatusBar, Platform, Alert, type EmitterSubscription} from 'react-native';
|
||||
import {type ComponentWillAppearEvent, type ImageResource, type LayoutOrientation, Navigation, type Options, OptionsModalPresentationStyle, type OptionsTopBarButton, type ScreenPoppedEvent, type EventSubscription, type ComponentDidAppearEvent} from 'react-native-navigation';
|
||||
import {type ComponentWillAppearEvent, type ImageResource, type LayoutOrientation, Navigation, type Options, OptionsModalPresentationStyle, type OptionsTopBarButton, type ScreenPoppedEvent, type EventSubscription} from 'react-native-navigation';
|
||||
import tinyColor from 'tinycolor2';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Events, Screens, Launch} from '@constants';
|
||||
import {NOT_READY, SCREENS_WITH_EXTRA_KEYBOARD} from '@constants/screens';
|
||||
import {NOT_READY} from '@constants/screens';
|
||||
import {getDefaultThemeByAppearance} from '@context/theme';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
|
|
@ -40,8 +39,15 @@ export function registerNavigationListeners() {
|
|||
Navigation.events().registerScreenPoppedListener(onPoppedListener),
|
||||
Navigation.events().registerCommandListener(onCommandListener),
|
||||
Navigation.events().registerComponentWillAppearListener(onScreenWillAppear),
|
||||
Navigation.events().registerComponentDidAppearListener(onScreenDidAppear),
|
||||
Navigation.events().registerComponentDidDisappearListener(onScreenDidDisappear),
|
||||
|
||||
/**
|
||||
* For the time being and until we add the emoji picker in the keyboard area
|
||||
* will keep Android as adjustResize cause useAnimatedKeyboard from reanimated
|
||||
* is reporting the wrong values when the keyboard was opened but we switch
|
||||
* to a different channel or thread.
|
||||
*/
|
||||
// Navigation.events().registerComponentDidAppearListener(onScreenDidAppear),
|
||||
// Navigation.events().registerComponentDidDisappearListener(onScreenDidDisappear),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -79,33 +85,12 @@ function onPoppedListener({componentId}: ScreenPoppedEvent) {
|
|||
NavigationStore.removeScreenFromStack(componentId as AvailableScreens);
|
||||
}
|
||||
|
||||
function setAndroidSoftKeyboard(screen: AvailableScreens) {
|
||||
if (Platform.OS !== 'android') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SCREENS_WITH_EXTRA_KEYBOARD.has(screen) || (isTablet() && screen === Screens.HOME)) {
|
||||
RNUtils.setSoftKeyboardToAdjustNothing();
|
||||
} else {
|
||||
RNUtils.setSoftKeyboardToAdjustResize();
|
||||
}
|
||||
}
|
||||
|
||||
function onScreenWillAppear(event: ComponentWillAppearEvent) {
|
||||
if (event.componentId === Screens.HOME) {
|
||||
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true);
|
||||
}
|
||||
}
|
||||
|
||||
function onScreenDidAppear(event: ComponentDidAppearEvent) {
|
||||
setAndroidSoftKeyboard(event.componentId as AvailableScreens);
|
||||
}
|
||||
|
||||
function onScreenDidDisappear() {
|
||||
const screen = NavigationStore.getVisibleScreen();
|
||||
setAndroidSoftKeyboard(screen);
|
||||
}
|
||||
|
||||
export const loginAnimationOptions = () => {
|
||||
const theme = getThemeFromState();
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
|||
import {postEphemeralCallResponseForPost} from '@actions/remote/apps';
|
||||
import OptionItem from '@components/option_item';
|
||||
import {useAppBinding} from '@hooks/apps';
|
||||
import {usePreventDoubleTap} from '@hooks/utils';
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
import {observeCurrentTeamId} from '@queries/servers/system';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import {isSystemMessage} from '@utils/post';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
|
@ -70,13 +70,13 @@ const AppBindingsPostOptions = ({bottomSheetId, serverUrl, post, teamId, binding
|
|||
};
|
||||
|
||||
const BindingOptionItem = ({binding, onPress}: {binding: AppBinding; onPress: (binding: AppBinding) => void}) => {
|
||||
const handlePress = useCallback(preventDoubleTap(() => {
|
||||
const handlePress = usePreventDoubleTap(useCallback(() => {
|
||||
onPress(binding);
|
||||
}), [binding, onPress]);
|
||||
}, [binding, onPress]));
|
||||
|
||||
return (
|
||||
<OptionItem
|
||||
label={binding.label}
|
||||
label={binding.label || ''}
|
||||
icon={binding.icon}
|
||||
action={handlePress}
|
||||
type='default'
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ function Header() {
|
|||
|
||||
const headerStyle = useMemo(() => ({...styles.header, marginLeft: canAddOtherServers ? MARGIN_WITH_SERVER_ICON : undefined}), [canAddOtherServers]);
|
||||
const onLogoutPress = useCallback(() => {
|
||||
alertServerLogout(serverDisplayName, () => logout(serverUrl), intl);
|
||||
}, [serverUrl, serverDisplayName]);
|
||||
alertServerLogout(serverDisplayName, () => logout(serverUrl, intl), intl);
|
||||
}, [serverDisplayName, intl, serverUrl]);
|
||||
|
||||
const onLabelPress = useCallback(() => {
|
||||
serverButtonRef.current?.openServers();
|
||||
|
|
|
|||
|
|
@ -125,8 +125,8 @@ const TermsOfService = ({
|
|||
|
||||
const closeTermsAndLogout = useCallback(() => {
|
||||
dismissOverlay(componentId);
|
||||
logout(serverUrl);
|
||||
}, [serverUrl, componentId]);
|
||||
logout(serverUrl, intl, {logoutOnAlert: true});
|
||||
}, [serverUrl, componentId, intl]);
|
||||
|
||||
const alertError = useCallback((retry: () => void) => {
|
||||
Alert.alert(
|
||||
|
|
|
|||
|
|
@ -190,18 +190,28 @@ function cleanStaticSelect(field: AppField): void {
|
|||
return;
|
||||
}
|
||||
|
||||
let value = option.value;
|
||||
if (!value) {
|
||||
value = option.label;
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
toRemove.unshift(i);
|
||||
return;
|
||||
}
|
||||
|
||||
if (usedLabels[label]) {
|
||||
toRemove.unshift(i);
|
||||
return;
|
||||
}
|
||||
|
||||
if (usedValues[option.value]) {
|
||||
if (usedValues[value]) {
|
||||
toRemove.unshift(i);
|
||||
return;
|
||||
}
|
||||
|
||||
usedLabels[label] = true;
|
||||
usedValues[option.value] = true;
|
||||
usedValues[value] = true;
|
||||
});
|
||||
|
||||
toRemove.forEach((i) => {
|
||||
|
|
@ -246,7 +256,7 @@ export function createCallRequest(
|
|||
};
|
||||
}
|
||||
|
||||
export const makeCallErrorResponse = (errMessage: string): AppCallResponse<any> => {
|
||||
export const makeCallErrorResponse = <T=unknown>(errMessage: string): AppCallResponse<T> => {
|
||||
return {
|
||||
type: AppCallResponseTypes.ERROR,
|
||||
text: errMessage,
|
||||
|
|
@ -312,7 +322,7 @@ function isAppCall(obj: unknown): obj is AppCall {
|
|||
|
||||
const call = obj as AppCall;
|
||||
|
||||
if (typeof call.path !== 'string') {
|
||||
if (call.path !== undefined && typeof call.path !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -348,7 +358,11 @@ function isAppSelectOption(v: unknown): v is AppSelectOption {
|
|||
|
||||
const option = v as AppSelectOption;
|
||||
|
||||
if (typeof option.label !== 'string' || typeof option.value !== 'string') {
|
||||
if (option.label !== undefined && typeof option.label !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (option.value !== undefined && typeof option.value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -366,7 +380,11 @@ function isAppField(v: unknown): v is AppField {
|
|||
|
||||
const field = v as AppField;
|
||||
|
||||
if (typeof field.name !== 'string' || typeof field.type !== 'string') {
|
||||
if (field.name !== undefined && typeof field.name !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (field.type !== undefined && typeof field.type !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -494,7 +512,11 @@ export function isAppBinding(obj: unknown): obj is AppBinding {
|
|||
|
||||
const binding = obj as AppBinding;
|
||||
|
||||
if (typeof binding.app_id !== 'string' || typeof binding.label !== 'string') {
|
||||
if (binding.app_id !== undefined && typeof binding.app_id !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (binding.label !== undefined && typeof binding.label !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,13 +92,13 @@ describe('isPostAction', () => {
|
|||
expect(isPostAction(nonObjectInput)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for missing id', () => {
|
||||
const invalidPostAction = {name: 'name'};
|
||||
test('returns false for wrong id type', () => {
|
||||
const invalidPostAction = {id: {}, name: 'name'};
|
||||
expect(isPostAction(invalidPostAction)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for missing name', () => {
|
||||
const invalidPostAction = {id: 'id'};
|
||||
test('returns false for wrong name type', () => {
|
||||
const invalidPostAction = {id: 'id', name: {}};
|
||||
expect(isPostAction(invalidPostAction)).toBe(false);
|
||||
});
|
||||
|
||||
|
|
@ -154,11 +154,6 @@ describe('isMessageAttachmentField', () => {
|
|||
expect(isMessageAttachmentField(nonObjectInput)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for missing title', () => {
|
||||
const invalidField = {value: 'value', short: true};
|
||||
expect(isMessageAttachmentField(invalidField)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for non-string title', () => {
|
||||
const invalidField = {title: 123, value: 'value', short: true};
|
||||
expect(isMessageAttachmentField(invalidField)).toBe(false);
|
||||
|
|
@ -169,8 +164,8 @@ describe('isMessageAttachmentField', () => {
|
|||
expect(isMessageAttachmentField(invalidField)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for missing value', () => {
|
||||
const invalidField = {title: 'title', short: true};
|
||||
test('returns false for invalid value', () => {
|
||||
const invalidField = {title: 'title', value: {toString: 123}, short: true};
|
||||
expect(isMessageAttachmentField(invalidField)).toBe(false);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -39,19 +39,11 @@ function isPostAction(v: unknown): v is PostAction {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!('id' in v)) {
|
||||
if ('id' in v && typeof v.id !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof v.id !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!('name' in v)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof v.name !== 'string') {
|
||||
if ('name' in v && typeof v.name !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +78,7 @@ function isPostAction(v: unknown): v is PostAction {
|
|||
return true;
|
||||
}
|
||||
|
||||
function isMessageAttachmentField(v: unknown) {
|
||||
function isMessageAttachmentField(v: unknown): v is MessageAttachmentField {
|
||||
if (typeof v !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -95,19 +87,11 @@ function isMessageAttachmentField(v: unknown) {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!('title' in v)) {
|
||||
if ('title' in v && typeof v.title !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof v.title !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!('value' in v)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof v.value === 'object' && v.value && 'toString' in v.value && typeof v.value.toString !== 'function') {
|
||||
if ('value' in v && typeof v.value === 'object' && v.value && 'toString' in v.value && typeof v.value.toString !== 'function') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -194,6 +178,24 @@ function isMessageAttachment(v: unknown): v is MessageAttachment {
|
|||
return true;
|
||||
}
|
||||
|
||||
export function filterOptions(options: PostActionOption[] | undefined) {
|
||||
return options?.reduce((acc: DialogOption[], option) => {
|
||||
let optionText = option.text;
|
||||
let optionValue = option.value;
|
||||
|
||||
if (optionText && !optionValue) {
|
||||
optionValue = optionText;
|
||||
} else if (optionValue && !optionText) {
|
||||
optionText = optionValue;
|
||||
}
|
||||
|
||||
if (optionText && optionValue) {
|
||||
acc.push({text: optionText, value: optionValue});
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export const testExports = {
|
||||
isMessageAttachment,
|
||||
isMessageAttachmentField,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"api.channel.add_guest.added": "{addedUsername} added to the channel as a guest by {username}.",
|
||||
"api.channel.add_member.added": "{addedUsername} added to the channel by {username}.",
|
||||
"api.channel.guest_join_channel.post_and_forget": "{username} joined the channel as a guest.",
|
||||
"apps.binding.default_button_name": "Submit",
|
||||
"apps.error": "Error: {error}",
|
||||
"apps.error.command.field_missing": "Required fields missing: `{fieldName}`.",
|
||||
"apps.error.command.same_channel": "Channel repeated for field `{fieldName}`: `{option}`.",
|
||||
|
|
@ -50,6 +51,7 @@
|
|||
"apps.error.parser.empty_value": "Empty values are not allowed.",
|
||||
"apps.error.parser.execute_non_leaf": "You must select a subcommand.",
|
||||
"apps.error.parser.missing_binding": "Missing command bindings.",
|
||||
"apps.error.parser.missing_field_name": "Field name is missing.",
|
||||
"apps.error.parser.missing_field_value": "Field value is missing.",
|
||||
"apps.error.parser.missing_list_end": "Expected list closing token.",
|
||||
"apps.error.parser.missing_quote": "Matching double quote expected before end of input.",
|
||||
|
|
@ -465,6 +467,12 @@
|
|||
"login.signIn": "Log In",
|
||||
"login.signingIn": "Logging In",
|
||||
"login.username": "Username",
|
||||
"logout.fail.cancel": "Cancel",
|
||||
"logout.fail.continue_anyway": "Continue Anyway",
|
||||
"logout.fail.message": "You’re not fully logged out. Some data may continue to be accessible to this device once the device goes back online. What do you want to do?",
|
||||
"logout.fail.message.forced": "We could not log you out of the server. Some data may continue to be accessible to this device once the device goes back online.",
|
||||
"logout.fail.ok": "OK",
|
||||
"logout.fail.title": "Logout not complete",
|
||||
"markdown.latex.error": "Latex render error",
|
||||
"markdown.max_nodes.error": "This message is too long to by shown fully on a mobile device. Please view it on desktop or contact an admin to increase this limit.",
|
||||
"markdown.parse_error": "An error occurred while parsing this text",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ func initSentryAppExt(){
|
|||
SentrySDK.start { options in
|
||||
options.dsn = SENTRY_DSN
|
||||
options.enableAppHangTracking = false
|
||||
options.enableCaptureFailedRequests = true
|
||||
options.enableCaptureFailedRequests = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
27C667A529523F0A00E590D5 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 27C667A429523F0A00E590D5 /* Sentry */; };
|
||||
27C667A9295241B600E590D5 /* Sentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C667A8295241B600E590D5 /* Sentry.swift */; };
|
||||
27C667AA295241B600E590D5 /* Sentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C667A8295241B600E590D5 /* Sentry.swift */; };
|
||||
413A0FD06EA0DB2B3F30403F /* libPods-NotificationService.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AEDDC97F55A1F6C056C6B1BC /* libPods-NotificationService.a */; };
|
||||
49AE370126D4455D00EF4E52 /* Gekidou in Frameworks */ = {isa = PBXBuildFile; productRef = 49AE370026D4455D00EF4E52 /* Gekidou */; };
|
||||
536CC6C323E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 536CC6C123E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m */; };
|
||||
58495E36BF1A4EAB93609E57 /* Metropolis-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 54956DEFEBB74EF78C3A6AE5 /* Metropolis-SemiBold.ttf */; };
|
||||
|
|
@ -379,6 +380,8 @@
|
|||
83ABFD0F2C1C90D90029685B /* calls_cheerful.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = calls_cheerful.mp3; path = ../assets/sounds/calls_cheerful.mp3; sourceTree = "<group>"; };
|
||||
83ABFD102C1C90D90029685B /* calls_calm.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = calls_calm.mp3; path = ../assets/sounds/calls_calm.mp3; sourceTree = "<group>"; };
|
||||
83ABFD112C1C90D90029685B /* calls_urgent.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = calls_urgent.mp3; path = ../assets/sounds/calls_urgent.mp3; sourceTree = "<group>"; };
|
||||
A10297DBBE728B2BF1BC045E /* Pods-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
AEDDC97F55A1F6C056C6B1BC /* libPods-NotificationService.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NotificationService.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BC977883E2624E05975CA65B /* OpenSans-Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-Regular.ttf"; path = "../assets/fonts/OpenSans-Regular.ttf"; sourceTree = "<group>"; };
|
||||
BE17F630DB5D41FD93F32D22 /* OpenSans-LightItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-LightItalic.ttf"; path = "../assets/fonts/OpenSans-LightItalic.ttf"; sourceTree = "<group>"; };
|
||||
C9A1070F2BBD7C8700753CDC /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
|
|
@ -388,6 +391,7 @@
|
|||
E5C16B14E1CE4868886A1A00 /* OpenSans-SemiBold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-SemiBold.ttf"; path = "../assets/fonts/OpenSans-SemiBold.ttf"; sourceTree = "<group>"; };
|
||||
E7BEDFCF1660521CCB3EADEF /* Pods-Mattermost.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mattermost.release.xcconfig"; path = "Target Support Files/Pods-Mattermost/Pods-Mattermost.release.xcconfig"; sourceTree = "<group>"; };
|
||||
FBBEC29EE2D3418D9AC33BD5 /* OpenSans-ExtraBoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-ExtraBoldItalic.ttf"; path = "../assets/fonts/OpenSans-ExtraBoldItalic.ttf"; sourceTree = "<group>"; };
|
||||
FF7D3AE3E5E892576497A111 /* Pods-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.release.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
|
@ -406,6 +410,7 @@
|
|||
files = (
|
||||
49AE370126D4455D00EF4E52 /* Gekidou in Frameworks */,
|
||||
27C667A329523ECA00E590D5 /* Sentry in Frameworks */,
|
||||
413A0FD06EA0DB2B3F30403F /* libPods-NotificationService.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -527,6 +532,8 @@
|
|||
children = (
|
||||
2E17B88EAD6181B9E9A24084 /* Pods-Mattermost.debug.xcconfig */,
|
||||
E7BEDFCF1660521CCB3EADEF /* Pods-Mattermost.release.xcconfig */,
|
||||
A10297DBBE728B2BF1BC045E /* Pods-NotificationService.debug.xcconfig */,
|
||||
FF7D3AE3E5E892576497A111 /* Pods-NotificationService.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -561,6 +568,7 @@
|
|||
7F43D6051F6BF9EB001FC614 /* libPods-Mattermost.a */,
|
||||
81061F4CBB31484A94D5A8EE /* libz.tbd */,
|
||||
7A83BBA09DE89A630126EA06 /* libPods-Mattermost.a */,
|
||||
AEDDC97F55A1F6C056C6B1BC /* libPods-NotificationService.a */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1080,9 +1088,11 @@
|
|||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 7F581D82221ED5C60099E66B /* Build configuration list for PBXNativeTarget "NotificationService" */;
|
||||
buildPhases = (
|
||||
656D4CFDAC0EC1C1E94D9329 /* [CP] Check Pods Manifest.lock */,
|
||||
7F581D2E221ED5C60099E66B /* Sources */,
|
||||
7F581D2F221ED5C60099E66B /* Frameworks */,
|
||||
7F581D30221ED5C60099E66B /* Resources */,
|
||||
FABCD84BD2A468FFB374B89C /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
|
@ -1473,6 +1483,28 @@
|
|||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Mattermost/Pods-Mattermost-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
656D4CFDAC0EC1C1E94D9329 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-NotificationService-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
A40F47AC8A062DFCF1B2A23E /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
|
@ -1529,6 +1561,24 @@
|
|||
shellPath = /bin/sh;
|
||||
shellScript = "./uploadDebugSymbols.sh\n";
|
||||
};
|
||||
FABCD84BD2A468FFB374B89C /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-NotificationService/Pods-NotificationService-resources.sh",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/CocoaLumberjack/CocoaLumberjackPrivacy.bundle",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/CocoaLumberjackPrivacy.bundle",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NotificationService/Pods-NotificationService-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
|
@ -1998,7 +2048,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 602;
|
||||
CURRENT_PROJECT_VERSION = 620;
|
||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||
ENABLE_BITCODE = NO;
|
||||
HEADER_SEARCH_PATHS = "$(inherited)";
|
||||
|
|
@ -2040,7 +2090,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 602;
|
||||
CURRENT_PROJECT_VERSION = 620;
|
||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||
ENABLE_BITCODE = NO;
|
||||
HEADER_SEARCH_PATHS = "$(inherited)";
|
||||
|
|
@ -2070,6 +2120,7 @@
|
|||
};
|
||||
7F581D3A221ED5C60099E66B /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A10297DBBE728B2BF1BC045E /* Pods-NotificationService.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
|
|
@ -2118,6 +2169,7 @@
|
|||
};
|
||||
7F581D3B221ED5C60099E66B /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = FF7D3AE3E5E892576497A111 /* Pods-NotificationService.release.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
|
|
@ -2183,7 +2235,7 @@
|
|||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 602;
|
||||
CURRENT_PROJECT_VERSION = 620;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
|
|
@ -2233,7 +2285,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 602;
|
||||
CURRENT_PROJECT_VERSION = 620;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#import <RNKeychain/RNKeychainManager.h>
|
||||
#import <ReactNativeNavigation/ReactNativeNavigation.h>
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
#import <TurboLogIOSNative/TurboLog.h>
|
||||
|
||||
#import "Mattermost-Swift.h"
|
||||
#import <os/log.h>
|
||||
|
|
@ -28,6 +29,15 @@ NSString* const NOTIFICATION_TEST_ACTION = @"test";
|
|||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
NSString *appGroupId = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"AppGroupIdentifier"];
|
||||
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroupId];
|
||||
containerURL = [containerURL URLByAppendingPathComponent:@"Logs"];
|
||||
NSError *error = nil;
|
||||
[TurboLog configureWithDailyRolling:FALSE maximumFileSize:1024*1024 maximumNumberOfFiles:2 logsDirectory:containerURL.path logsFilename:@"MMLogs" error:&error];
|
||||
if (error) {
|
||||
NSLog(@"Failed to configure TurboLog: %@", error.localizedDescription);
|
||||
}
|
||||
[TurboLog writeWithLogLevel:TurboLogLevelInfo message:@[@"Configured turbolog"]];
|
||||
OrientationManager.shared.delegate = self;
|
||||
|
||||
// Clear keychain on first run in case of reinstallation
|
||||
|
|
@ -35,7 +45,7 @@ NSString* const NOTIFICATION_TEST_ACTION = @"test";
|
|||
|
||||
RNKeychainManager *keychain = [[RNKeychainManager alloc] init];
|
||||
NSArray<NSString*> *servers = [keychain getAllServersForInternetPasswords];
|
||||
NSLog(@"Servers %@", servers);
|
||||
[TurboLog writeWithLogLevel:TurboLogLevelInfo message:@[@"Servers", servers]];
|
||||
for (NSString *server in servers) {
|
||||
[keychain deleteCredentialsForServer:server withOptions:nil];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.25.0</string>
|
||||
<string>2.26.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>602</string>
|
||||
<string>620</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.25.0</string>
|
||||
<string>2.26.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>602</string>
|
||||
<string>620</string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>OpenSans-Bold.ttf</string>
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.25.0</string>
|
||||
<string>2.26.2</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>602</string>
|
||||
<string>620</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import Gekidou
|
|||
import UserNotifications
|
||||
import Intents
|
||||
import os.log
|
||||
import TurboLogIOSNative
|
||||
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
|
|
@ -10,38 +11,47 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
override init() {
|
||||
super.init()
|
||||
initSentryAppExt()
|
||||
do {
|
||||
let appGroupId = Bundle.main.object(forInfoDictionaryKey: "AppGroupIdentifier") as! String
|
||||
let containerUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId)
|
||||
try TurboLogIOSNative.TurboLogger.configure(dailyRolling: false, maximumFileSize: 1024*1024, maximumNumberOfFiles: 2, logsDirectory: containerUrl!.appendingPathComponent("Logs").path, logsFilename: "MMLogs")
|
||||
} catch {
|
||||
os_log(OSLogType.default, "Failed to configure TurboLogger: %{public}@", String(describing: error))
|
||||
}
|
||||
}
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
self.contentHandler = contentHandler
|
||||
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: received notification")
|
||||
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
||||
if let bestAttemptContent = bestAttemptContent {
|
||||
PushNotification.default.postNotificationReceipt(bestAttemptContent, completionHandler: {[weak self] notification in
|
||||
if let notification = notification {
|
||||
self?.bestAttemptContent = notification
|
||||
if (!PushNotification.default.verifySignatureFromNotification(notification)) {
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: signature not verified. Will call sendInvalidNotificationIntent")
|
||||
self?.sendInvalidNotificationIntent()
|
||||
return
|
||||
}
|
||||
if (Gekidou.Preferences.default.object(forKey: "ApplicationIsRunning") as? String != "true") {
|
||||
PushNotification.default.fetchAndStoreDataForPushNotification(bestAttemptContent, withContentHandler: {notification in
|
||||
os_log(OSLogType.default, "Mattermost Notifications: processed data for db. Will call sendMessageIntent")
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: processed data for db. Will call sendMessageIntent")
|
||||
self?.sendMessageIntent()
|
||||
})
|
||||
} else {
|
||||
bestAttemptContent.badge = nil
|
||||
os_log(OSLogType.default, "Mattermost Notifications: app in use, no data processed. Will call sendMessageIntent")
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: app in use, no data processed. Will call sendMessageIntent")
|
||||
self?.sendMessageIntent()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
os_log(OSLogType.default, "Mattermost Notifications: notification receipt seems to be empty, will call sendMessageIntent")
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: notification receipt seems to be empty, will call sendMessageIntent")
|
||||
self?.sendMessageIntent()
|
||||
})
|
||||
} else {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: bestAttemptContent seems to be empty, will call sendMessageIntent")
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: bestAttemptContent seems to be empty, will call sendMessageIntent")
|
||||
sendMessageIntent()
|
||||
}
|
||||
}
|
||||
|
|
@ -49,8 +59,8 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
override func serviceExtensionTimeWillExpire() {
|
||||
// Called just before the extension will be terminated by the system.
|
||||
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
||||
os_log(OSLogType.default, "Mattermost Notifications: service extension time expired")
|
||||
os_log(OSLogType.default, "Mattermost Notifications: calling sendMessageIntent before expiration")
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: service extension time expired")
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: calling sendMessageIntent before expiration")
|
||||
sendMessageIntent()
|
||||
}
|
||||
|
||||
|
|
@ -62,13 +72,13 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
|
||||
guard let serverUrl = notification.userInfo["server_url"] as? String
|
||||
else {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: No intent created. will call contentHandler to present notification")
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: No intent created. will call contentHandler to present notification")
|
||||
self.contentHandler?(notification)
|
||||
return
|
||||
}
|
||||
|
||||
let overrideIconUrl = notification.userInfo["override_icon_url"] as? String
|
||||
os_log(OSLogType.default, "Mattermost Notifications: Fetching profile Image in server %{public}@ for sender %{public}@", serverUrl, senderId ?? overrideUsername ?? "no sender is set")
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: Fetching profile Image in server", serverUrl, "for sender", senderId ?? overrideUsername ?? "no sender is set")
|
||||
if senderId != nil || overrideIconUrl != nil {
|
||||
PushNotification.default.fetchProfileImageSync(serverUrl, senderId: senderId ?? "", overrideIconUrl: overrideIconUrl) {[weak self] data in
|
||||
self?.sendMessageIntentCompletion(data)
|
||||
|
|
@ -81,7 +91,7 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
|
||||
private func sendInvalidNotificationIntent() {
|
||||
guard let notification = bestAttemptContent else { return }
|
||||
os_log(OSLogType.default, "Mattermost Notifications: creating invalid intent")
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: creating invalid intent")
|
||||
|
||||
bestAttemptContent?.body = NSLocalizedString( "native.ios.notifications.not_verified",
|
||||
value: "We could not verify this notification with the server",
|
||||
|
|
@ -103,15 +113,15 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
interaction.donate { error in
|
||||
if error != nil {
|
||||
self.contentHandler?(notification)
|
||||
os_log(OSLogType.default, "Mattermost Notifications: sendMessageIntent intent error %{public}@", error! as CVarArg)
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: sendMessageIntent intent error", error! as CVarArg)
|
||||
}
|
||||
|
||||
do {
|
||||
let updatedContent = try notification.updating(from: intent)
|
||||
os_log(OSLogType.default, "Mattermost Notifications: present updated notification")
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: present updated notification")
|
||||
self.contentHandler?(updatedContent)
|
||||
} catch {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: something failed updating the notification %{public}@", error as CVarArg)
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: something failed updating the notification", error as CVarArg)
|
||||
self.contentHandler?(notification)
|
||||
}
|
||||
}
|
||||
|
|
@ -125,7 +135,7 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
if #available(iOSApplicationExtension 15.0, *),
|
||||
let imgData = avatarData,
|
||||
let channelId = notification.userInfo["channel_id"] as? String {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: creating intent")
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: creating intent")
|
||||
|
||||
let isCRTEnabled = notification.userInfo["is_crt_enabled"] as? Bool ?? false
|
||||
let rootId = notification.userInfo["root_id"] as? String ?? ""
|
||||
|
|
@ -171,15 +181,15 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
interaction.donate { error in
|
||||
if error != nil {
|
||||
self.contentHandler?(notification)
|
||||
os_log(OSLogType.default, "Mattermost Notifications: sendMessageIntent intent error %{public}@", error! as CVarArg)
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: sendMessageIntent intent error", error! as CVarArg)
|
||||
}
|
||||
|
||||
do {
|
||||
let updatedContent = try notification.updating(from: intent)
|
||||
os_log(OSLogType.default, "Mattermost Notifications: present updated notification")
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: present updated notification")
|
||||
self.contentHandler?(updatedContent)
|
||||
} catch {
|
||||
os_log(OSLogType.default, "Mattermost Notifications: something failed updating the notification %{public}@", error as CVarArg)
|
||||
TurboLogIOSNative.TurboLogger.write(level: .info, message: "Mattermost Notifications: something failed updating the notification", error as CVarArg)
|
||||
self.contentHandler?(notification)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ target 'Mattermost' do
|
|||
pod 'React-jsi', :path => '../node_modules/react-native/ReactCommon/jsi', :modular_headers => true
|
||||
pod 'React-jsc', :path => '../node_modules/react-native/ReactCommon/jsc', :modular_headers => true
|
||||
pod 'simdjson', path: '../node_modules/@nozbe/simdjson', :modular_headers => true
|
||||
pod 'CocoaLumberjack', :modular_headers => true
|
||||
pod 'TurboLogIOSNative', :git => 'https://github.com/larkox/react-native-turbo-log-ios-native.git', :modular_headers => true
|
||||
|
||||
post_install do |installer|
|
||||
# https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
|
||||
|
|
@ -59,3 +61,8 @@ target 'Mattermost' do
|
|||
# end
|
||||
end
|
||||
end
|
||||
|
||||
target 'NotificationService' do
|
||||
pod 'TurboLogIOSNative', :git => 'https://github.com/larkox/react-native-turbo-log-ios-native.git', :modular_headers => true
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ PODS:
|
|||
- DoubleConversion (1.1.6)
|
||||
- EXApplication (6.0.1):
|
||||
- ExpoModulesCore
|
||||
- EXConstants (17.0.3):
|
||||
- EXConstants (17.0.5):
|
||||
- ExpoModulesCore
|
||||
- Expo (52.0.19):
|
||||
- ExpoModulesCore
|
||||
|
|
@ -15,9 +15,9 @@ PODS:
|
|||
- ExpoModulesCore
|
||||
- ExpoDevice (7.0.1):
|
||||
- ExpoModulesCore
|
||||
- ExpoFileSystem (18.0.6):
|
||||
- ExpoFileSystem (18.0.7):
|
||||
- ExpoModulesCore
|
||||
- ExpoImage (2.0.3):
|
||||
- ExpoImage (2.0.4):
|
||||
- ExpoModulesCore
|
||||
- SDWebImage (~> 5.19.1)
|
||||
- SDWebImageSVGCoder (~> 1.7.0)
|
||||
|
|
@ -59,7 +59,7 @@ PODS:
|
|||
- hermes-engine/Pre-built (= 0.76.5)
|
||||
- hermes-engine/Pre-built (0.76.5)
|
||||
- HMSegmentedControl (1.5.6)
|
||||
- JitsiWebRTC (124.0.1)
|
||||
- JitsiWebRTC (124.0.2)
|
||||
- mattermost-hardware-keyboard (0.0.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
|
|
@ -81,8 +81,7 @@ PODS:
|
|||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- mattermost-react-native-turbo-log (0.5.0):
|
||||
- CocoaLumberjack (~> 3.8.5)
|
||||
- mattermost-react-native-turbo-log (0.6.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
|
|
@ -102,6 +101,7 @@ PODS:
|
|||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- TurboLogIOSNative
|
||||
- Yoga
|
||||
- mattermost-rnutils (0.0.0):
|
||||
- DoubleConversion
|
||||
|
|
@ -1454,7 +1454,7 @@ PODS:
|
|||
- Yoga
|
||||
- react-native-netinfo (11.4.1):
|
||||
- React-Core
|
||||
- react-native-network-client (1.8.1):
|
||||
- react-native-network-client (1.8.2):
|
||||
- Alamofire (~> 5.10.2)
|
||||
- DoubleConversion
|
||||
- glog
|
||||
|
|
@ -2094,6 +2094,8 @@ PODS:
|
|||
- SocketRocket (0.7.1)
|
||||
- Starscream (4.0.8)
|
||||
- SwiftyJSON (5.0.2)
|
||||
- TurboLogIOSNative (0.1.0):
|
||||
- CocoaLumberjack (~> 3.8.5)
|
||||
- WatermelonDB (0.28.0-1):
|
||||
- React
|
||||
- simdjson
|
||||
|
|
@ -2101,6 +2103,7 @@ PODS:
|
|||
|
||||
DEPENDENCIES:
|
||||
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
||||
- CocoaLumberjack
|
||||
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||
- EXApplication (from `../node_modules/expo-application/ios`)
|
||||
- EXConstants (from `../node_modules/expo-constants/ios`)
|
||||
|
|
@ -2212,6 +2215,7 @@ DEPENDENCIES:
|
|||
- RNSVG (from `../node_modules/react-native-svg`)
|
||||
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
|
||||
- "simdjson (from `../node_modules/@nozbe/simdjson`)"
|
||||
- TurboLogIOSNative (from `https://github.com/larkox/react-native-turbo-log-ios-native.git`)
|
||||
- "WatermelonDB (from `../node_modules/@nozbe/watermelondb`)"
|
||||
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
||||
|
||||
|
|
@ -2450,23 +2454,30 @@ EXTERNAL SOURCES:
|
|||
:path: "../node_modules/react-native-vector-icons"
|
||||
simdjson:
|
||||
:path: "../node_modules/@nozbe/simdjson"
|
||||
TurboLogIOSNative:
|
||||
:git: https://github.com/larkox/react-native-turbo-log-ios-native.git
|
||||
WatermelonDB:
|
||||
:path: "../node_modules/@nozbe/watermelondb"
|
||||
Yoga:
|
||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||
|
||||
CHECKOUT OPTIONS:
|
||||
TurboLogIOSNative:
|
||||
:commit: 44fa9abe32a74ce6206d6237c639e6382495d210
|
||||
:git: https://github.com/larkox/react-native-turbo-log-ios-native.git
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||
boost: 1dca942403ed9342f98334bf4c3621f011aa7946
|
||||
CocoaLumberjack: 6a459bc897d6d80bd1b8c78482ec7ad05dffc3f0
|
||||
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
|
||||
EXApplication: 88ebf1a95f85faa5d2df160016d61f2d060e9438
|
||||
EXConstants: 277129d9a42ba2cf1fad375e7eaa9939005c60be
|
||||
EXConstants: e25c3f3ef5d1cf7a6ef911ebf7a3ab94d8d06d82
|
||||
Expo: 848501c300b37b3fa7330c58a5353e07e03d5c3d
|
||||
ExpoCrypto: 483fc758365923f89ddaab4327c21cae9534841d
|
||||
ExpoDevice: 449822f2c45660c1ce83283dd51c67fe5404996c
|
||||
ExpoFileSystem: a38e1bb58d77f41717293a7b73ebd4014d8cb8dd
|
||||
ExpoImage: 2c1f4adee95d033894c53346c721361736583728
|
||||
ExpoFileSystem: 818e82dbb71175414d1ca310e926c48ff0d07348
|
||||
ExpoImage: 18ec1a3e5cd96ee6162d988be3085e18113e143f
|
||||
ExpoLinearGradient: 18148bd38f98fa0229c1f9df23393b32ab6d2d32
|
||||
ExpoModulesCore: c9cb309323aee3c048099bb40ad0226e3fcf0c56
|
||||
ExpoStoreReview: f38ed71ee04dfc02dde63a05f925f001cebba5d8
|
||||
|
|
@ -2477,9 +2488,9 @@ SPEC CHECKSUMS:
|
|||
glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a
|
||||
hermes-engine: 06a9c6900587420b90accc394199527c64259db4
|
||||
HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352
|
||||
JitsiWebRTC: d0ae5fd6a81e771bfd82c2ee6c6bb542ebd65ee8
|
||||
JitsiWebRTC: b47805ab5668be38e7ee60e2258f49badfe8e1d0
|
||||
mattermost-hardware-keyboard: 510850877516c61f71f06c95377b96263af1b80d
|
||||
mattermost-react-native-turbo-log: 4eff5879e0d263f8146666a6124d66f8e8fcc0ae
|
||||
mattermost-react-native-turbo-log: 9e2c4f2e2b278a1541e5c2eb99ed9cd2af601fe2
|
||||
mattermost-rnutils: 88291c01fc351a93c553b6a3bce44b17f8d38233
|
||||
RCT-Folly: 84578c8756030547307e4572ab1947de1685c599
|
||||
RCTDeprecation: fb7d408617e25d7f537940000d766d60149c5fea
|
||||
|
|
@ -2518,7 +2529,7 @@ SPEC CHECKSUMS:
|
|||
react-native-emm: f6003bebdf4fef4feef7c61f96a5c174f43c6b5f
|
||||
react-native-image-picker: 130fad649d07e4eec8faaed361d3bba570e1e5ff
|
||||
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
|
||||
react-native-network-client: f2996a6440c422e64e787bdf8c8e3f992be1cee6
|
||||
react-native-network-client: fdbd7f5f4c2818d6b90b4dcf9613d0a54ab41303
|
||||
react-native-notifications: 3bafa1237ae8a47569a84801f17d80242fe9f6a5
|
||||
react-native-paste-input: d33daa183942fa57f34dff2088805a60895f14d2
|
||||
react-native-performance: 125a96c145e29918b55b45ce25cbba54f1e24dcd
|
||||
|
|
@ -2576,9 +2587,10 @@ SPEC CHECKSUMS:
|
|||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
Starscream: 19b5533ddb925208db698f0ac508a100b884a1b9
|
||||
SwiftyJSON: f5b1bf1cd8dd53cd25887ac0eabcfd92301c6a5a
|
||||
TurboLogIOSNative: 45f6d20ba17ee39f8106e718e93baf43b2d76b7b
|
||||
WatermelonDB: 438a8dd16910dfe622095c44b5d1dbe11cbba6ba
|
||||
Yoga: fcc198acd4a55599b3468cfb6ebc526baff5f06e
|
||||
|
||||
PODFILE CHECKSUM: f61c6130110ff18ebe025edfebd33129a0601fec
|
||||
PODFILE CHECKSUM: 0e437947a8e6696940c2efdfd7205ab54d67b7b8
|
||||
|
||||
COCOAPODS: 1.16.1
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.mattermost.rnutils
|
|||
|
||||
import android.app.Activity
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.view.WindowManager
|
||||
import androidx.core.view.OnApplyWindowInsetsListener
|
||||
import com.facebook.react.bridge.Arguments
|
||||
|
|
@ -128,6 +129,9 @@ class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext) {
|
|||
|
||||
fun setSoftKeyboardToAdjustNothing() {
|
||||
val currentActivity: Activity = reactContext.currentActivity ?: return
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||
return
|
||||
}
|
||||
|
||||
currentActivity.runOnUiThread {
|
||||
currentActivity.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
|
||||
|
|
@ -136,6 +140,9 @@ class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext) {
|
|||
|
||||
fun setSoftKeyboardToAdjustResize() {
|
||||
val currentActivity: Activity = reactContext.currentActivity ?: return
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||
return
|
||||
}
|
||||
|
||||
currentActivity.runOnUiThread {
|
||||
currentActivity.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
|
|
|
|||
34
package-lock.json
generated
34
package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "mattermost-mobile",
|
||||
"version": "2.25.0",
|
||||
"version": "2.26.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
@ -21,9 +21,9 @@
|
|||
"@mattermost/compass-icons": "0.1.48",
|
||||
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
|
||||
"@mattermost/react-native-emm": "1.5.1",
|
||||
"@mattermost/react-native-network-client": "1.8.1",
|
||||
"@mattermost/react-native-network-client": "1.8.2",
|
||||
"@mattermost/react-native-paste-input": "0.8.1",
|
||||
"@mattermost/react-native-turbo-log": "0.5.0",
|
||||
"@mattermost/react-native-turbo-log": "0.6.0",
|
||||
"@mattermost/rnshare": "file:./libraries/@mattermost/rnshare",
|
||||
"@mattermost/rnutils": "file:./libraries/@mattermost/rnutils",
|
||||
"@msgpack/msgpack": "2.8.0",
|
||||
|
|
@ -43,6 +43,7 @@
|
|||
"@sentry/react-native": "6.4.0",
|
||||
"@stream-io/flat-list-mvcp": "0.10.3",
|
||||
"@voximplant/react-native-foreground-service": "3.0.2",
|
||||
"APNG4Android": "github:mattermost/APNG4Android#15a7e93d59542d18c1e562d49b3f4896dd1643a6",
|
||||
"base-64": "1.0.0",
|
||||
"commonmark": "npm:@mattermost/commonmark@0.30.1-4",
|
||||
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#81b5d27509652bae50b4b510ede777dd3bd923cf",
|
||||
|
|
@ -53,7 +54,7 @@
|
|||
"expo-application": "6.0.1",
|
||||
"expo-crypto": "14.0.1",
|
||||
"expo-device": "7.0.1",
|
||||
"expo-image": "2.0.3",
|
||||
"expo-image": "2.0.4",
|
||||
"expo-linear-gradient": "14.0.1",
|
||||
"expo-store-review": "8.0.0",
|
||||
"expo-video-thumbnails": "9.0.2",
|
||||
|
|
@ -4631,9 +4632,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@mattermost/react-native-network-client": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.8.1.tgz",
|
||||
"integrity": "sha512-CrIVcIkSJej2Dg2162z2U4qyL0QIV/LYuHd0wXh9wnrcGBjk8Dt0TDhUkUaTe8V1YKYVQiJU7vsL9GlDMxVuXQ==",
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.8.2.tgz",
|
||||
"integrity": "sha512-wveW7wuXqwjxt3udip1H3Uw72ApNmSo6ii2qx9HZ81+LX3FnhBJhlB6q7xF6KP8DUEOxRZHvPWwAtGxYKWI0lw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"validator": "13.12.0",
|
||||
|
|
@ -4658,10 +4659,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@mattermost/react-native-turbo-log": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@mattermost/react-native-turbo-log/-/react-native-turbo-log-0.5.0.tgz",
|
||||
"integrity": "sha512-8X7GbEKr7QDdSTckpxGGogQ2+IDJ8xcUOmLylo1NhmvCPvdqAsBvVLNZngx0zjoLl28IP3H24bR9fpXGsnGiag==",
|
||||
"license": "MIT",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@mattermost/react-native-turbo-log/-/react-native-turbo-log-0.6.0.tgz",
|
||||
"integrity": "sha512-yV0GWLXivvGwEjhMKR0meMsP5VWA7Br/fz70UC4/IZd9gPmBvfNpznMHzddwekvhYZ6Hqg0jbqK1SNitkvJwFg==",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
|
|
@ -8488,6 +8488,11 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/APNG4Android": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "git+ssh://git@github.com/mattermost/APNG4Android.git#15a7e93d59542d18c1e562d49b3f4896dd1643a6",
|
||||
"integrity": "sha512-Aqhpb5ZvjJJPmCjwWnekWHMNOx3P4DuRzmEh+lHEdH2FiB1f/nnbQ7ex5jSXPRIEFMlLY5YnsMLsfbZ6RdZNtA=="
|
||||
},
|
||||
"node_modules/appdirsjs": {
|
||||
"version": "1.2.7",
|
||||
"license": "MIT"
|
||||
|
|
@ -12143,10 +12148,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/expo-image": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expo-image/-/expo-image-2.0.3.tgz",
|
||||
"integrity": "sha512-+YnHTQv8jbXaut3FY7TDhNiSiGZ927C329mHvTZWV4Fyj32/Hjhhmk7dqq9I6LrA0nqBBiJjFj3u6VdHvCBnZg==",
|
||||
"license": "MIT",
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/expo-image/-/expo-image-2.0.4.tgz",
|
||||
"integrity": "sha512-oIVSlV/BpBJbI9Anf8RjraGJXDWSJxNKxdXkYBJ31XrQaUstOM7OmsVKjOK40ZFZHIz3rx5SBJWK5w52Uz+/bg==",
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "mattermost-mobile",
|
||||
"version": "2.25.0",
|
||||
"version": "2.26.2",
|
||||
"description": "Mattermost Mobile with React Native",
|
||||
"repository": "git@github.com:mattermost/mattermost-mobile.git",
|
||||
"author": "Mattermost, Inc.",
|
||||
|
|
@ -22,9 +22,9 @@
|
|||
"@mattermost/compass-icons": "0.1.48",
|
||||
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
|
||||
"@mattermost/react-native-emm": "1.5.1",
|
||||
"@mattermost/react-native-network-client": "1.8.1",
|
||||
"@mattermost/react-native-network-client": "1.8.2",
|
||||
"@mattermost/react-native-paste-input": "0.8.1",
|
||||
"@mattermost/react-native-turbo-log": "0.5.0",
|
||||
"@mattermost/react-native-turbo-log": "0.6.0",
|
||||
"@mattermost/rnshare": "file:./libraries/@mattermost/rnshare",
|
||||
"@mattermost/rnutils": "file:./libraries/@mattermost/rnutils",
|
||||
"@msgpack/msgpack": "2.8.0",
|
||||
|
|
@ -44,6 +44,7 @@
|
|||
"@sentry/react-native": "6.4.0",
|
||||
"@stream-io/flat-list-mvcp": "0.10.3",
|
||||
"@voximplant/react-native-foreground-service": "3.0.2",
|
||||
"APNG4Android": "github:mattermost/APNG4Android#15a7e93d59542d18c1e562d49b3f4896dd1643a6",
|
||||
"base-64": "1.0.0",
|
||||
"commonmark": "npm:@mattermost/commonmark@0.30.1-4",
|
||||
"commonmark-react-renderer": "github:mattermost/commonmark-react-renderer#81b5d27509652bae50b4b510ede777dd3bd923cf",
|
||||
|
|
@ -54,7 +55,7 @@
|
|||
"expo-application": "6.0.1",
|
||||
"expo-crypto": "14.0.1",
|
||||
"expo-device": "7.0.1",
|
||||
"expo-image": "2.0.3",
|
||||
"expo-image": "2.0.4",
|
||||
"expo-linear-gradient": "14.0.1",
|
||||
"expo-store-review": "8.0.0",
|
||||
"expo-video-thumbnails": "9.0.2",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,21 @@
|
|||
diff --git a/node_modules/expo-image/android/build.gradle b/node_modules/expo-image/android/build.gradle
|
||||
index a85fb5e..ac568c1 100644
|
||||
--- a/node_modules/expo-image/android/build.gradle
|
||||
+++ b/node_modules/expo-image/android/build.gradle
|
||||
@@ -44,7 +44,12 @@ dependencies {
|
||||
kapt "com.github.bumptech.glide:compiler:${GLIDE_VERSION}"
|
||||
api 'com.caverock:androidsvg-aar:1.4'
|
||||
|
||||
- implementation "com.github.penfeizhou.android.animation:glide-plugin:3.0.2"
|
||||
+ implementation project(':frameanimation')
|
||||
+ implementation project(':gif')
|
||||
+ implementation("com.github.penfeizhou.android.animation:glide-plugin:3.0.2") {
|
||||
+ exclude group: 'com.github.penfeizhou.android.animation', module: 'frameanimation'
|
||||
+ exclude group: 'com.github.penfeizhou.android.animation', module: 'gif'
|
||||
+ }
|
||||
implementation "com.github.bumptech.glide:avif-integration:${GLIDE_VERSION}"
|
||||
|
||||
api 'com.github.bumptech.glide:okhttp3-integration:4.11.0'
|
||||
diff --git a/node_modules/expo-image/android/src/main/java/expo/modules/image/okhttp/ExpoImageOkHttpClientGlideModule.kt b/node_modules/expo-image/android/src/main/java/expo/modules/image/okhttp/ExpoImageOkHttpClientGlideModule.kt
|
||||
index 071907c..edf10c1 100644
|
||||
--- a/node_modules/expo-image/android/src/main/java/expo/modules/image/okhttp/ExpoImageOkHttpClientGlideModule.kt
|
||||
74
patches/react-native-keyboard-aware-scroll-view+0.9.5.patch
Normal file
74
patches/react-native-keyboard-aware-scroll-view+0.9.5.patch
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
diff --git a/node_modules/react-native-keyboard-aware-scroll-view/index.d.ts b/node_modules/react-native-keyboard-aware-scroll-view/index.d.ts
|
||||
index bf03b26..e11063b 100644
|
||||
--- a/node_modules/react-native-keyboard-aware-scroll-view/index.d.ts
|
||||
+++ b/node_modules/react-native-keyboard-aware-scroll-view/index.d.ts
|
||||
@@ -69,6 +69,7 @@ interface KeyboardAwareProps {
|
||||
* @memberof KeyboardAwareProps
|
||||
*/
|
||||
enableOnAndroid?: boolean
|
||||
+ noPaddingBottomOnAndroid?: boolean
|
||||
|
||||
/**
|
||||
* Adds an extra offset when focusing the TextInputs.
|
||||
diff --git a/node_modules/react-native-keyboard-aware-scroll-view/lib/KeyboardAwareHOC.js b/node_modules/react-native-keyboard-aware-scroll-view/lib/KeyboardAwareHOC.js
|
||||
index 03f46af..f58175f 100644
|
||||
--- a/node_modules/react-native-keyboard-aware-scroll-view/lib/KeyboardAwareHOC.js
|
||||
+++ b/node_modules/react-native-keyboard-aware-scroll-view/lib/KeyboardAwareHOC.js
|
||||
@@ -57,6 +57,7 @@ export type KeyboardAwareHOCProps = {
|
||||
update?: Function,
|
||||
contentContainerStyle?: any,
|
||||
enableOnAndroid?: boolean,
|
||||
+ noPaddingBottomOnAndroid?: boolean,
|
||||
innerRef?: Function,
|
||||
...keyboardAwareHOCTypeEvents
|
||||
}
|
||||
@@ -92,6 +93,7 @@ export type ScrollIntoViewOptions = ?{
|
||||
|
||||
export type KeyboardAwareHOCOptions = ?{
|
||||
enableOnAndroid: boolean,
|
||||
+ noPaddingBottomOnAndroid: boolean,
|
||||
contentContainerStyle: ?Object,
|
||||
enableAutomaticScroll: boolean,
|
||||
extraHeight: number,
|
||||
@@ -113,6 +115,7 @@ function getDisplayName(WrappedComponent: React$Component) {
|
||||
|
||||
const ScrollIntoViewDefaultOptions: KeyboardAwareHOCOptions = {
|
||||
enableOnAndroid: false,
|
||||
+ noPaddingBottomOnAndroid: false,
|
||||
contentContainerStyle: undefined,
|
||||
enableAutomaticScroll: true,
|
||||
extraHeight: _KAM_EXTRA_HEIGHT,
|
||||
@@ -181,6 +184,7 @@ function KeyboardAwareHOC(
|
||||
update: PropTypes.func,
|
||||
contentContainerStyle: PropTypes.any,
|
||||
enableOnAndroid: PropTypes.bool,
|
||||
+ noPaddingBottomOnAndroid: PropTypes.bool,
|
||||
innerRef: PropTypes.func,
|
||||
...keyboardEventPropTypes
|
||||
}
|
||||
@@ -193,7 +197,8 @@ function KeyboardAwareHOC(
|
||||
enableResetScrollToCoords: hocOptions.enableResetScrollToCoords,
|
||||
keyboardOpeningTime: hocOptions.keyboardOpeningTime,
|
||||
viewIsInsideTabBar: hocOptions.viewIsInsideTabBar,
|
||||
- enableOnAndroid: hocOptions.enableOnAndroid
|
||||
+ enableOnAndroid: hocOptions.enableOnAndroid,
|
||||
+ noPaddingBottomOnAndroid: hocOptions.noPaddingBottomOnAndroid
|
||||
}
|
||||
|
||||
constructor(props: KeyboardAwareHOCProps) {
|
||||
@@ -523,13 +528,13 @@ function KeyboardAwareHOC(
|
||||
}
|
||||
|
||||
render() {
|
||||
- const { enableOnAndroid, contentContainerStyle, onScroll } = this.props
|
||||
+ const { enableOnAndroid, contentContainerStyle, noPaddingBottomOnAndroid, onScroll } = this.props
|
||||
let newContentContainerStyle
|
||||
if (Platform.OS === 'android' && enableOnAndroid) {
|
||||
newContentContainerStyle = [].concat(contentContainerStyle).concat({
|
||||
paddingBottom:
|
||||
((contentContainerStyle || {}).paddingBottom || 0) +
|
||||
- this.state.keyboardSpace
|
||||
+ noPaddingBottomOnAndroid ? 0 : this.state.keyboardSpace
|
||||
})
|
||||
}
|
||||
const refProps = { [hocOptions.refPropName]: this._handleRef }
|
||||
16
types/api/apps.d.ts
vendored
16
types/api/apps.d.ts
vendored
|
|
@ -23,15 +23,15 @@ type AppsState = {
|
|||
};
|
||||
|
||||
type AppBinding = {
|
||||
app_id: string;
|
||||
location: string;
|
||||
app_id?: string;
|
||||
location?: string;
|
||||
icon?: string;
|
||||
|
||||
// Label is the (usually short) primary text to display at the location.
|
||||
// - For LocationPostMenu is the menu item text.
|
||||
// - For LocationChannelHeader is the dropdown text.
|
||||
// - For LocationCommand is the name of the command
|
||||
label: string;
|
||||
label?: string;
|
||||
|
||||
// Hint is the secondary text to display
|
||||
// - LocationPostMenu: not used
|
||||
|
|
@ -60,7 +60,7 @@ type AppCallValues = {
|
|||
};
|
||||
|
||||
type AppCall = {
|
||||
path: string;
|
||||
path?: string;
|
||||
expand?: AppExpand;
|
||||
state?: any;
|
||||
};
|
||||
|
|
@ -152,8 +152,8 @@ type AppFormValue = string | boolean | number | AppSelectOption | AppSelectOptio
|
|||
type AppFormValues = {[name: string]: AppFormValue};
|
||||
|
||||
type AppSelectOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
label?: string;
|
||||
value?: string;
|
||||
icon_data?: string;
|
||||
};
|
||||
|
||||
|
|
@ -163,8 +163,8 @@ type AppFieldType = string;
|
|||
type AppField = {
|
||||
|
||||
// Name is the name of the JSON field to use.
|
||||
name: string;
|
||||
type: AppFieldType;
|
||||
name?: string;
|
||||
type?: AppFieldType;
|
||||
is_required?: boolean;
|
||||
readonly?: boolean;
|
||||
|
||||
|
|
|
|||
4
types/api/integrations.d.ts
vendored
4
types/api/integrations.d.ts
vendored
|
|
@ -105,8 +105,8 @@ type PostAction = {
|
|||
};
|
||||
|
||||
type PostActionOption = {
|
||||
text: string;
|
||||
value: string;
|
||||
text?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
type PostActionIntegration = {
|
||||
|
|
|
|||
38
types/api/posts.d.ts
vendored
38
types/api/posts.d.ts
vendored
|
|
@ -107,29 +107,29 @@ type ProcessedPosts = {
|
|||
}
|
||||
|
||||
type MessageAttachment = {
|
||||
id: number;
|
||||
fallback: string;
|
||||
color: string;
|
||||
pretext: string;
|
||||
author_name: string;
|
||||
author_link: string;
|
||||
author_icon: string;
|
||||
title: string;
|
||||
title_link: string;
|
||||
text: string;
|
||||
fields: MessageAttachmentField[];
|
||||
image_url: string;
|
||||
thumb_url: string;
|
||||
footer: string;
|
||||
footer_icon: string;
|
||||
timestamp: number | string;
|
||||
id?: number;
|
||||
fallback?: string;
|
||||
color?: string;
|
||||
pretext?: string;
|
||||
author_name?: string;
|
||||
author_link?: string;
|
||||
author_icon?: string;
|
||||
title?: string;
|
||||
title_link?: string;
|
||||
text?: string;
|
||||
fields?: MessageAttachmentField[];
|
||||
image_url?: string;
|
||||
thumb_url?: string;
|
||||
footer?: string;
|
||||
footer_icon?: string;
|
||||
timestamp?: number | string;
|
||||
actions?: PostAction[];
|
||||
};
|
||||
|
||||
type MessageAttachmentField = {
|
||||
title: string;
|
||||
value: any;
|
||||
short: boolean;
|
||||
title?: string;
|
||||
value?: any;
|
||||
short?: boolean;
|
||||
}
|
||||
|
||||
type PostSearchParams = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue