Merge branch 'main' into nandr

This commit is contained in:
Mattermost Build 2023-06-26 07:14:58 +03:00 committed by GitHub
commit 29d0ae921f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
120 changed files with 3771 additions and 3059 deletions

View file

@ -1,617 +0,0 @@
version: 2.1
orbs:
owasp: entur/owasp@0.0.10
node: circleci/node@5.0.3
executors:
android:
parameters:
resource_class:
default: xlarge
type: string
environment:
NODE_OPTIONS: --max_old_space_size=12000
NODE_ENV: production
BABEL_ENV: production
docker:
- image: cimg/android:2022.09.2-node
working_directory: ~/mattermost-mobile
resource_class: <<parameters.resource_class>>
ios:
parameters:
resource_class:
default: medium
type: string
environment:
NODE_OPTIONS: --max_old_space_size=12000
NODE_ENV: production
BABEL_ENV: production
macos:
xcode: "14.2.0"
working_directory: ~/mattermost-mobile
shell: /bin/bash --login -o pipefail
resource_class: <<parameters.resource_class>>
commands:
checkout-private:
description: "Checkout the private repo with build env vars"
steps:
- add_ssh_keys:
fingerprints:
- "03:1c:a7:07:35:bc:57:e4:1d:6c:e1:2c:4b:be:09:6d"
- run:
name: Clone the mobile private repo
command: git clone git@github.com:mattermost/mattermost-mobile-private.git ~/mattermost-mobile-private
fastlane-dependencies:
description: "Get Fastlane dependencies"
parameters:
for:
type: string
steps:
- restore_cache:
name: Restore Fastlane cache
key: v1-gems-<< parameters.for >>-{{ checksum "fastlane/Gemfile.lock" }}-{{ arch }}
- run:
working_directory: fastlane
name: Download Fastlane dependencies
command: bundle install --path vendor/bundle
- save_cache:
name: Save Fastlane cache
key: v1-gems-<< parameters.for >>-{{ checksum "fastlane/Gemfile.lock" }}-{{ arch }}
paths:
- fastlane/vendor/bundle
gradle-dependencies:
description: "Get Gradle dependencies"
steps:
- restore_cache:
name: Restore Gradle cache
key: v1-gradle-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
- run:
working_directory: android
name: Download Gradle dependencies
command: ./gradlew dependencies
- save_cache:
name: Save Gradle cache
paths:
- ~/.gradle
key: v1-gradle-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
assets:
description: "Generate app assets"
steps:
- restore_cache:
name: Restore assets cache
key: v1-assets-{{ checksum "assets/base/config.json" }}-{{ arch }}
- run:
name: Generate assets
command: node ./scripts/generate-assets.js
- run:
name: Compass Icons
environment:
COMPASS_ICONS: "node_modules/@mattermost/compass-icons/font/compass-icons.ttf"
command: |
cp "$COMPASS_ICONS" "assets/fonts/"
cp "$COMPASS_ICONS" "android/app/src/main/assets/fonts"
- save_cache:
name: Save assets cache
key: v1-assets-{{ checksum "assets/base/config.json" }}-{{ arch }}
paths:
- dist
npm-dependencies:
description: "Get JavaScript dependencies"
steps:
- node/install:
node-version: '18.7.0'
- restore_cache:
name: Restore npm cache
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
- run:
name: Getting JavaScript dependencies
command: |
NODE_ENV=development npm ci --ignore-scripts
node node_modules/\@sentry/cli/scripts/install.js
- save_cache:
name: Save npm cache
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
paths:
- node_modules
- run:
name: "Patch dependencies"
command: npx patch-package
pods-dependencies:
description: "Get cocoapods dependencies"
steps:
- restore_cache:
name: Restore cocoapods specs and pods
key: v1-cocoapods-{{ checksum "ios/Podfile.lock" }}-{{ arch }}
- run:
name: iOS gems
command: npm run ios-gems
- run:
name: Getting cocoapods dependencies
command: npm run pod-install
- save_cache:
name: Save cocoapods specs and pods cache
key: v1-cocoapods-{{ checksum "ios/Podfile.lock" }}-{{ arch }}
paths:
- ios/Pods
- ~/.cocoapods
build-android:
description: "Build the android app"
steps:
- checkout:
path: ~/mattermost-mobile
- checkout-private
- npm-dependencies
- assets
- fastlane-dependencies:
for: android
- gradle-dependencies
- run:
name: Append Keystore to build Android
command: |
cp ~/mattermost-mobile-private/android/${STORE_FILE} android/app/${STORE_FILE}
echo "" | tee -a android/gradle.properties > /dev/null
echo MATTERMOST_RELEASE_STORE_FILE=${STORE_FILE} | tee -a android/gradle.properties > /dev/null
echo ${STORE_ALIAS} | tee -a android/gradle.properties > /dev/null
echo ${STORE_PASSWORD} | tee -a android/gradle.properties > /dev/null
- run:
name: Jetify android libraries
command: ./node_modules/.bin/jetify
- run:
working_directory: fastlane
name: Run fastlane to build android
no_output_timeout: 30m
command: export TERM=xterm && bundle exec fastlane android build
build-ios:
description: "Build the iOS app"
steps:
- checkout:
path: ~/mattermost-mobile
- npm-dependencies
- pods-dependencies
- assets
- fastlane-dependencies:
for: ios
- run:
working_directory: fastlane
name: Run fastlane to build iOS
no_output_timeout: 30m
command: |
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
export TERM=xterm && bundle exec fastlane ios build
deploy-to-store:
description: "Deploy build to store"
parameters:
task:
type: string
target:
type: string
file:
type: string
env:
type: string
steps:
- attach_workspace:
at: ~/
- run:
name: <<parameters.task>>
working_directory: fastlane
command: <<parameters.env>> bundle exec fastlane <<parameters.target>> deploy file:$HOME/mattermost-mobile/<<parameters.file>>
persist:
description: "Persist mattermost-mobile directory"
steps:
- persist_to_workspace:
root: ~/
paths:
- mattermost-mobile*
save:
description: "Save binaries artifacts"
parameters:
filename:
type: string
steps:
- run:
name: Copying artifacts
command: |
mkdir /tmp/artifacts;
cp ~/mattermost-mobile/<<parameters.filename>> /tmp/artifacts;
- store_artifacts:
path: /tmp/artifacts
jobs:
test:
working_directory: ~/mattermost-mobile
docker:
- image: cimg/node:16.14.2
steps:
- checkout:
path: ~/mattermost-mobile
- npm-dependencies
- assets
- run:
name: Check styles
command: npm run check
- run:
name: Running Tests
command: npm test
- run:
name: Check i18n
command: ./scripts/precommit/i18n.sh
check-deps:
parameters:
cve_data_directory:
type: string
default: "~/.owasp/dependency-check-data"
working_directory: ~/mattermost-mobile
executor: owasp/default
environment:
version_url: "https://jeremylong.github.io/DependencyCheck/current.txt"
executable_url: "https://dl.bintray.com/jeremy-long/owasp/dependency-check-VERSION-release.zip"
steps:
- checkout
- restore_cache:
name: Restore npm cache
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
- run:
name: Checkout config
command: cd .. && git clone https://github.com/mattermost/security-automation-config
- run:
name: Install Go
command: sudo apt-get update && sudo apt-get install golang
- owasp/with_commandline:
steps:
# Taken from https://github.com/entur/owasp-orb/blob/master/src/%40orb.yml#L349-L361
- owasp/generate_cache_keys:
cache_key: commmandline-default-cache-key-v7
- owasp/restore_owasp_cache
- run:
name: Update OWASP Dependency-Check Database
command: |
if ! ~/.owasp/dependency-check/bin/dependency-check.sh --data << parameters.cve_data_directory >> --updateonly; then
# Update failed, probably due to a bad DB version; delete cached DB and try again
rm -rv ~/.owasp/dependency-check-data/*.db
~/.owasp/dependency-check/bin/dependency-check.sh --data << parameters.cve_data_directory >> --updateonly
fi
- owasp/store_owasp_cache:
cve_data_directory: <<parameters.cve_data_directory>>
- run:
name: Run OWASP Dependency-Check Analyzer
command: |
~/.owasp/dependency-check/bin/dependency-check.sh \
--data << parameters.cve_data_directory >> --format ALL --noupdate --enableExperimental \
--propertyfile ../security-automation-config/dependency-check/dependencycheck.properties \
--suppression ../security-automation-config/dependency-check/suppression.xml \
--suppression ../security-automation-config/dependency-check/suppression.$CIRCLE_PROJECT_REPONAME.xml \
--scan './**/*' || true
- owasp/collect_reports:
persist_to_workspace: false
- run:
name: Post results to Mattermost
command: go run ../security-automation-config/dependency-check/post_results.go
build-android-beta:
executor: android
steps:
- build-android
- persist
- save:
filename: "*.apk"
build-android-release:
executor: android
steps:
- build-android
- persist
- save:
filename: "*.apk"
build-android-pr:
executor: android
environment:
BRANCH_TO_BUILD: ${CIRCLE_BRANCH}
steps:
- build-android
- save:
filename: "*.apk"
build-android-unsigned:
executor: android
steps:
- checkout:
path: ~/mattermost-mobile
- npm-dependencies
- assets
- fastlane-dependencies:
for: android
- gradle-dependencies
- run:
name: Jetify Android libraries
command: ./node_modules/.bin/jetify
- run:
working_directory: fastlane
name: Run fastlane to build unsigned android
no_output_timeout: 30m
command: bundle exec fastlane android unsigned
- persist
- save:
filename: "*.apk"
build-ios-beta:
executor:
name: ios
resource_class: large
steps:
- build-ios
- persist
- save:
filename: "*.ipa"
build-ios-release:
executor: ios
steps:
- build-ios
- persist
- save:
filename: "*.ipa"
build-ios-pr:
executor: ios
environment:
BRANCH_TO_BUILD: ${CIRCLE_BRANCH}
steps:
- build-ios
- save:
filename: "*.ipa"
build-ios-unsigned:
executor: ios
steps:
- checkout:
path: ~/mattermost-mobile
- npm-dependencies
- pods-dependencies
- assets
- fastlane-dependencies:
for: ios
- run:
working_directory: fastlane
name: Run fastlane to build unsigned iOS
no_output_timeout: 30m
command: |
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
bundle exec fastlane ios unsigned
- persist_to_workspace:
root: ~/
paths:
- mattermost-mobile/*.ipa
- save:
filename: "*.ipa"
build-ios-simulator:
executor: ios
steps:
- checkout:
path: ~/mattermost-mobile
- npm-dependencies
- pods-dependencies
- assets
- fastlane-dependencies:
for: ios
- run:
working_directory: fastlane
name: Run fastlane to build unsigned x86_64 iOS app for iPhone simulator
no_output_timeout: 30m
command: |
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
bundle exec fastlane ios simulator
- persist_to_workspace:
root: ~/
paths:
- mattermost-mobile/Mattermost-simulator-x86_64.app.zip
- save:
filename: "Mattermost-simulator-x86_64.app.zip"
deploy-android-release:
executor:
name: android
resource_class: medium
steps:
- deploy-to-store:
task: "Deploy to Google Play"
target: android
file: "*.apk"
env: "SUPPLY_TRACK=beta"
deploy-android-beta:
executor:
name: android
resource_class: medium
steps:
- deploy-to-store:
task: "Deploy to Google Play"
target: android
file: "*.apk"
env: "SUPPLY_TRACK=alpha"
deploy-ios-release:
executor: ios
steps:
- deploy-to-store:
task: "Deploy to TestFlight"
target: ios
file: "*.ipa"
env: ""
deploy-ios-beta:
executor: ios
steps:
- deploy-to-store:
task: "Deploy to TestFlight"
target: ios
file: "*.ipa"
env: ""
github-release:
executor:
name: android
resource_class: medium
steps:
- attach_workspace:
at: ~/
- run:
name: Create GitHub release
working_directory: fastlane
command: bundle exec fastlane github
workflows:
version: 2
build:
jobs:
- test
# - check-deps:
# context: sast-webhook
# requires:
# - test
- build-android-release:
context: mattermost-mobile-android-release
requires:
- test
filters:
branches:
only:
- /^build-release-\d+$/
- /^build-android-release-\d+$/
- deploy-android-release:
context: mattermost-mobile-android-release
requires:
- build-android-release
filters:
branches:
only:
- /^build-release-\d+$/
- /^build-android-release-\d+$/
- build-android-beta:
context: mattermost-mobile-android-beta
filters:
branches:
only:
- /^build-\d+$/
- /^build-android-\d+$/
- /^build-android-beta-\d+$/
- deploy-android-beta:
context: mattermost-mobile-android-beta
requires:
- build-android-beta
filters:
branches:
only:
- /^build-\d+$/
- /^build-android-\d+$/
- /^build-android-beta-\d+$/
- build-ios-release:
context: mattermost-mobile-ios-release
requires:
- test
filters:
branches:
only:
- /^build-release-\d+$/
- /^build-ios-release-\d+$/
- deploy-ios-release:
context: mattermost-mobile-ios-release
requires:
- build-ios-release
filters:
branches:
only:
- /^build-release-\d+$/
- /^build-ios-release-\d+$/
- build-ios-beta:
context: mattermost-mobile-ios-beta
filters:
branches:
only:
- /^build-\d+$/
- /^build-ios-\d+$/
- /^build-ios-beta-\d+$/
- deploy-ios-beta:
context: mattermost-mobile-ios-beta
requires:
- build-ios-beta
filters:
branches:
only:
- /^build-\d+$/
- /^build-ios-\d+$/
- /^build-ios-beta-\d+$/
- build-android-pr:
context: mattermost-mobile-android-pr
requires:
- test
filters:
branches:
only: /^(build|android)-pr-.*/
- build-ios-pr:
context: mattermost-mobile-ios-pr
requires:
- test
filters:
branches:
only: /^(build|ios)-pr-.*/
- build-android-unsigned:
context: mattermost-mobile-unsigned
requires:
- test
filters:
tags:
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
branches:
only: unsigned
- build-ios-unsigned:
context: mattermost-mobile-unsigned
requires:
- test
filters:
tags:
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
branches:
only: unsigned
- build-ios-simulator:
context: mattermost-mobile-unsigned
requires:
- test
filters:
branches:
only:
- /^build-\d+$/
- /^build-release-\d+$/
- /^build-ios-sim-\d+$/
- github-release:
context: mattermost-mobile-unsigned
requires:
- build-android-unsigned
- build-ios-unsigned
filters:
tags:
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
branches:
only: unsigned

View file

@ -0,0 +1,49 @@
name: prepare-android-build
description: Action to prepare environment for android build
inputs:
sign:
description: Flag to enable android package signing
default: "true"
runs:
using: composite
steps:
- name: ci/prepare-mobile-build
uses: ./.github/actions/prepare-mobile-build
# Disable this since we are not caching anything for now
# - name: ci/install-gradle-dependencies
# shell: bash
# working-directory: android
# run: |
# echo "::group::install-gradle-dependencies"
# ./gradlew dependencies
# echo "::endgroup::"
- name: ci/jetify-android-libraries
shell: bash
run: |
echo "::group::jetify-android-libraries"
./node_modules/.bin/jetify
echo "::endgroup::"
- name: ci/checkout-private-repo
if: ${{ inputs.sign == 'true' }}
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
with:
repository: mattermost/mattermost-mobile-private
token: ${{ env.MATTERMOST_BUILD_GH_TOKEN }}
path: ${{ github.workspace }}/mattermost-mobile-private
- name: ci/append-keystore-to-android-build-for-signing
if: ${{ inputs.sign == 'true' }}
shell: bash
run: |
echo "::group::append-keystore-to-android-build-for-signing"
cp ${{ github.workspace }}/mattermost-mobile-private/android/${STORE_FILE} android/app/${STORE_FILE}
echo "" | tee -a android/gradle.properties > /dev/null
echo MATTERMOST_RELEASE_STORE_FILE=${STORE_FILE} | tee -a android/gradle.properties > /dev/null
echo ${STORE_ALIAS} | tee -a android/gradle.properties > /dev/null
echo ${STORE_PASSWORD} | tee -a android/gradle.properties > /dev/null
echo "::endgroup::"

View file

@ -0,0 +1,25 @@
name: prepare-ios-build
description: Action to prepare environment for ios build
runs:
using: composite
steps:
- name: ci/install-os-deps
env:
HOMEBREW_NO_AUTO_UPDATE: "1"
shell: bash
run: |
echo "::group::install-os-deps"
brew install watchman
echo "::endgroup::"
- name: ci/prepare-mobile-build
uses: ./.github/actions/prepare-mobile-build
- name: ci/install-pods-dependencies
shell: bash
run: |
echo "::group::install-pods-dependencies"
npm run ios-gems
npm run pod-install
echo "::endgroup::"

View file

@ -0,0 +1,20 @@
name: prepare-mobile-build
description: Action to prepare environment for mobile build
runs:
using: composite
steps:
- uses: ruby/setup-ruby@9669f3ee51dc3f4eda8447ab696b3ab19a90d14b # v1.144.0
with:
ruby-version: "2.7.7"
- name: ci/setup-fastlane-dependencies
shell: bash
run: |
echo "::group::setup-fastlane-dependencies"
bundle install
echo "::endgroup::"
working-directory: ./fastlane
- name: ci/prepare-node-deps
uses: ./.github/actions/prepare-node-deps

View file

@ -0,0 +1,46 @@
name: deps
description: Common deps for mobile repo
runs:
using: composite
steps:
- name: ci/setup-node
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
with:
node-version: "${{ env.NODE_VERSION }}"
cache: "npm"
cache-dependency-path: package-lock.json
- name: ci/install-npm-dependencies
shell: bash
env:
NODE_ENV: development
run: |
echo "::group::install-npm-dependencies"
npm ci --ignore-scripts
node node_modules/\@sentry/cli/scripts/install.js
echo "::endgroup::"
- name: ci/patch-npm-dependencies
shell: bash
run: |
echo "::group::patch-npm-dependencies"
npx patch-package
echo "::endgroup::"
- name: ci/generate-assets
shell: bash
run: |
echo "::group::generate-assets"
node ./scripts/generate-assets.js
echo "::endgroup::"
- name: ci/import-compass-icon
shell: bash
env:
COMPASS_ICONS: "node_modules/@mattermost/compass-icons/font/compass-icons.ttf"
run: |
echo "::group::import-compass-icon"
cp "$COMPASS_ICONS" "assets/fonts/"
cp "$COMPASS_ICONS" "android/app/src/main/assets/fonts"
echo "::endgroup::"

27
.github/actions/test/action.yaml vendored Normal file
View file

@ -0,0 +1,27 @@
name: test
description: Common tests for mobile repo
runs:
using: composite
steps:
- name: ci/prepare-node-deps
uses: ./.github/actions/prepare-node-deps
- name: ci/check-styles
shell: bash
run: |
echo "::group::check-styles"
npm run check
echo "::endgroup::"
- name: ci/run-tests
shell: bash
run: |
echo "::group::run-tests"
npm test
echo "::endgroup::"
- name: ci/check-i18n
shell: bash
run: |
echo "::group::check-i18n"
./scripts/precommit/i18n.sh
echo "::endgroup::"

View file

@ -0,0 +1,61 @@
---
name: build-android-beta
on:
push:
branches:
- build-beta-[0-9]+
- build-android-[0-9]+
- build-android-beta-[0-9]+
env:
NODE_VERSION: 18.7.0
TERM: xterm
jobs:
test:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/test
uses: ./.github/actions/test
build-and-deploy-android-beta:
runs-on: ubuntu-22.04
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-android-build
uses: ./.github/actions/prepare-android-build
env:
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
- name: ci/build-and-deploy-android-beta
env:
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_BETA_MATTERMOST_WEBHOOK_URL }}"
SENTRY_AUTH_TOKEN: "${{ secrets.MM_MOBILE_SENTRY_AUTH_TOKEN }}"
SENTRY_DSN_ANDROID: ${{ secrets.MM_MOBILE_BETA_SENTRY_DSN_ANDROID }}
SUPPLY_JSON_KEY: ${{ github.workspace }}/mattermost-mobile-private/android/mattermost-credentials.json
run: |
echo "::group::Build"
bundle exec fastlane android build --env android.beta
echo "::endgroup::"
echo "::group::Deploy to Play Store"
bundle exec fastlane android deploy file:"${{ github.workspace }}/*.apk" --env android.beta
echo "::endgroup::"
working-directory: ./fastlane
- name: ci/upload-android-beta-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: android-build-beta-${{ github.run_id }}
path: "*.apk"

View file

@ -0,0 +1,60 @@
---
name: build-android-release
on:
push:
branches:
- build-release-[0-9]+
- build-release-android-[0-9]+
env:
NODE_VERSION: 18.7.0
TERM: xterm
jobs:
test:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/test
uses: ./.github/actions/test
build-and-deploy-android-release:
runs-on: ubuntu-22.04
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-android-build
uses: ./.github/actions/prepare-android-build
env:
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
- name: ci/build-and-deploy-android-release
env:
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_RELEASE_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_RELEASE_AWS_SECRET_ACCESS_KEY }}"
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_RELEASE_MATTERMOST_WEBHOOK_URL }}"
SENTRY_AUTH_TOKEN: "${{ secrets.MM_MOBILE_SENTRY_AUTH_TOKEN }}"
SENTRY_DSN_ANDROID: ${{ secrets.MM_MOBILE_RELEASE_SENTRY_DSN_ANDROID }}
SUPPLY_JSON_KEY: ${{ github.workspace }}/mattermost-mobile-private/android/mattermost-credentials.json
run: |
echo "::group::Build"
bundle exec fastlane android build --env android.release
echo "::endgroup::"
echo "::group::Deploy to Play Store"
bundle exec fastlane android deploy file:"${{ github.workspace }}/*.apk" --env android.release
echo "::endgroup::"
working-directory: ./fastlane
- name: ci/upload-android-release-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: android-build-release-${{ github.run_id }}
path: "*.apk"

99
.github/workflows/build-ios-beta.yml vendored Normal file
View file

@ -0,0 +1,99 @@
---
name: build-ios-beta
on:
push:
branches:
- build-beta-[0-9]+
- build-beta-ios-[0-9]+
- build-beta-sim-[0-9]+
env:
NODE_VERSION: 18.7.0
TERM: xterm
jobs:
test:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/test
uses: ./.github/actions/test
build-ios-simulator:
runs-on: macos-12
if: ${{ !contains(github.ref_name, 'beta-ios') }}
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-ios-build
uses: ./.github/actions/prepare-ios-build
- name: ci/build-ios-simulator
env:
TAG: "${{ github.ref_name }}"
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_BETA_MATTERMOST_WEBHOOK_URL }}"
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
run: bundle exec fastlane ios simulator --env ios.simulator
working-directory: ./fastlane
- name: ci/upload-ios-pr-simulator
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: ios-build-simulator-${{ github.run_id }}
path: Mattermost-simulator-x86_64.app.zip
build-and-deploy-ios-beta:
runs-on: macos-12
if: ${{ !contains(github.ref_name, 'beta-sim') }}
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/output-ssh-private-key
shell: bash
run: |
SSH_KEY_PATH=~/.ssh/id_ed25519
mkdir -p ~/.ssh
echo -e '${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}' > ${SSH_KEY_PATH}
chmod 0600 ${SSH_KEY_PATH}
ssh-keygen -y -f ${SSH_KEY_PATH} > ${SSH_KEY_PATH}.pub
- name: ci/prepare-ios-build
uses: ./.github/actions/prepare-ios-build
- name: ci/build-and-deploy-ios-beta
env:
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_BETA_MATTERMOST_WEBHOOK_URL }}"
FASTLANE_TEAM_ID: "${{ secrets.MM_MOBILE_FASTLANE_TEAM_ID }}"
IOS_API_ISSUER_ID: "${{ secrets.MM_MOBILE_IOS_API_ISSUER_ID }}"
IOS_API_KEY: "${{ secrets.MM_MOBILE_IOS_API_KEY }}"
IOS_API_KEY_ID: "${{ secrets.MM_MOBILE_IOS_API_KEY_ID }}"
MATCH_GIT_URL: "${{ secrets.MM_MOBILE_MATCH_GIT_URL }}"
MATCH_PASSWORD: "${{ secrets.MM_MOBILE_MATCH_PASSWORD }}"
SENTRY_AUTH_TOKEN: "${{ secrets.MM_MOBILE_SENTRY_AUTH_TOKEN }}"
SENTRY_DSN_IOS: "${{ secrets.MM_MOBILE_BETA_SENTRY_DSN_IOS }}"
run: |
echo "::group::Build"
bundle exec fastlane ios build --env ios.beta
echo "::endgroup::"
echo "::group::Deploy to TestFlight"
bundle exec fastlane ios deploy file:"${{ github.workspace }}/*.ipa" --env ios.beta
echo "::endgroup::"
working-directory: ./fastlane
- name: ci/upload-ios-beta-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: ios-build-beta-${{ github.run_id }}
path: "*.ipa"

99
.github/workflows/build-ios-release.yml vendored Normal file
View file

@ -0,0 +1,99 @@
---
name: build-ios-release
on:
push:
branches:
- build-release-[0-9]+
- build-release-ios-[0-9]+
- build-release-sim-[0-9]+
env:
NODE_VERSION: 18.7.0
TERM: xterm
jobs:
test:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/test
uses: ./.github/actions/test
build-and-deploy-ios-release:
runs-on: macos-12
if: ${{ !contains(github.ref_name, 'release-sim') }}
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-ios-build
uses: ./.github/actions/prepare-ios-build
- name: ci/output-ssh-private-key
shell: bash
run: |
SSH_KEY_PATH=~/.ssh/id_ed25519
mkdir -p ~/.ssh
echo -e '${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}' > ${SSH_KEY_PATH}
chmod 0600 ${SSH_KEY_PATH}
ssh-keygen -y -f ${SSH_KEY_PATH} > ${SSH_KEY_PATH}.pub
- name: ci/build-and-deploy-ios-release
env:
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_RELEASE_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_RELEASE_AWS_SECRET_ACCESS_KEY }}"
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_RELEASE_MATTERMOST_WEBHOOK_URL }}"
FASTLANE_TEAM_ID: "${{ secrets.MM_MOBILE_FASTLANE_TEAM_ID }}"
IOS_API_ISSUER_ID: "${{ secrets.MM_MOBILE_IOS_API_ISSUER_ID }}"
IOS_API_KEY: "${{ secrets.MM_MOBILE_IOS_API_KEY }}"
IOS_API_KEY_ID: "${{ secrets.MM_MOBILE_IOS_API_KEY_ID }}"
MATCH_GIT_URL: "${{ secrets.MM_MOBILE_MATCH_GIT_URL }}"
MATCH_PASSWORD: "${{ secrets.MM_MOBILE_MATCH_PASSWORD }}"
SENTRY_AUTH_TOKEN: "${{ secrets.MM_MOBILE_SENTRY_AUTH_TOKEN }}"
SENTRY_DSN_IOS: ${{ secrets.MM_MOBILE_RELEASE_SENTRY_DSN_IOS }}
run: |
echo "::group::Build"
bundle exec fastlane ios build --env ios.release
echo "::endgroup::"
echo "::group::Deploy to TestFlight"
bundle exec fastlane ios deploy file:"${{ github.workspace }}/*.ipa" --env ios.release
echo "::endgroup::"
working-directory: ./fastlane
- name: ci/upload-ios-release-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: ios-build-release-${{ github.run_id }}
path: "*.ipa"
build-ios-simulator:
runs-on: macos-12
if: ${{ !contains(github.ref_name , 'release-ios') }}
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-ios-build
uses: ./.github/actions/prepare-ios-build
- name: ci/build-ios-simulator
env:
TAG: "${{ github.ref_name }}"
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_BETA_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_BETA_AWS_SECRET_ACCESS_KEY }}"
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_BETA_MATTERMOST_WEBHOOK_URL }}"
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
run: bundle exec fastlane ios simulator --env ios.simulator
working-directory: ./fastlane
- name: ci/upload-ios-pr-simulator
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: ios-build-simulator-${{ github.run_id }}
path: Mattermost-simulator-x86_64.app.zip

95
.github/workflows/build-pr.yml vendored Normal file
View file

@ -0,0 +1,95 @@
---
name: build-pr
on:
push:
branches:
- build-pr-*
- build-pr-android-*
- build-pr-ios-*
env:
NODE_VERSION: 18.7.0
TERM: xterm
jobs:
test:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/test
uses: ./.github/actions/test
build-ios-pr:
runs-on: macos-12
if: ${{ !contains(github.ref_name, 'android') }}
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-ios-build
uses: ./.github/actions/prepare-ios-build
- name: ci/output-ssh-private-key
shell: bash
run: |
SSH_KEY_PATH=~/.ssh/id_ed25519
mkdir -p ~/.ssh
echo -e '${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}' > ${SSH_KEY_PATH}
chmod 0600 ${SSH_KEY_PATH}
ssh-keygen -y -f ${SSH_KEY_PATH} > ${SSH_KEY_PATH}.pub
- name: ci/build-ios-pr
env:
BRANCH_TO_BUILD: "${{ github.ref_name }}"
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_PR_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_PR_AWS_SECRET_ACCESS_KEY }}"
FASTLANE_TEAM_ID: "${{ secrets.MM_MOBILE_FASTLANE_TEAM_ID }}"
IOS_API_ISSUER_ID: "${{ secrets.MM_MOBILE_IOS_API_ISSUER_ID }}"
IOS_API_KEY: "${{ secrets.MM_MOBILE_IOS_API_KEY }}"
IOS_API_KEY_ID: "${{ secrets.MM_MOBILE_IOS_API_KEY_ID }}"
MATCH_GIT_URL: "${{ secrets.MM_MOBILE_MATCH_GIT_URL }}"
MATCH_PASSWORD: "${{ secrets.MM_MOBILE_MATCH_PASSWORD }}"
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_PR_MATTERMOST_WEBHOOK_URL }}"
run: bundle exec fastlane ios build --env ios.pr
working-directory: ./fastlane
- name: ci/upload-ios-pr-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: ios-build-pr-${{ github.run_id }}
path: "*.ipa"
build-android-pr:
runs-on: ubuntu-22.04
if: ${{ !contains(github.ref_name, 'ios') }}
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-android-build
uses: ./.github/actions/prepare-android-build
env:
STORE_FILE: "${{ secrets.MM_MOBILE_STORE_FILE }}"
STORE_ALIAS: "${{ secrets.MM_MOBILE_STORE_ALIAS }}"
STORE_PASSWORD: "${{ secrets.MM_MOBILE_STORE_PASSWORD }}"
MATTERMOST_BUILD_GH_TOKEN: "${{ secrets.MATTERMOST_BUILD_GH_TOKEN }}"
- name: ci/build-android-pr
env:
BRANCH_TO_BUILD: "${{ github.ref_name }}"
AWS_ACCESS_KEY_ID: "${{ secrets.MM_MOBILE_PR_AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.MM_MOBILE_PR_AWS_SECRET_ACCESS_KEY }}"
MATTERMOST_WEBHOOK_URL: "${{ secrets.MM_MOBILE_PR_MATTERMOST_WEBHOOK_URL }}"
run: bundle exec fastlane android build --env android.pr
working-directory: ./fastlane
- name: ci/upload-android-pr-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: android-build-pr-${{ github.run_id }}
path: "*.apk"

17
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,17 @@
---
name: ci
on:
pull_request:
env:
NODE_VERSION: 18.7.0
TERM: xterm
jobs:
test:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/test
uses: ./.github/actions/test

100
.github/workflows/github-release.yml vendored Normal file
View file

@ -0,0 +1,100 @@
---
name: github-release
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+*
jobs:
test:
runs-on: ubuntu-22.04
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/test
uses: ./.github/actions/test
build-ios-unsigned:
runs-on: macos-12
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-ios-build
uses: ./.github/actions/prepare-ios-build
- name: ci/output-ssh-private-key
shell: bash
run: |
SSH_KEY_PATH=~/.ssh/id_ed25519
mkdir -p ~/.ssh
echo -e '${{ secrets.MM_MOBILE_PRIVATE_DEPLOY_KEY }}' > ${SSH_KEY_PATH}
chmod 0600 ${SSH_KEY_PATH}
ssh-keygen -y -f ${SSH_KEY_PATH} > ${SSH_KEY_PATH}.pub
- name: ci/build-ios-unsigned
env:
TAG: "${{ github.ref_name }}"
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
run: bundle exec fastlane ios unsigned
working-directory: ./fastlane
- name: ci/upload-ios-unsigned
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
path: Mattermost-unsigned.ipa
name: Mattermost-unsigned.ipa
build-android-unsigned:
runs-on: ubuntu-22.04
needs:
- test
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- name: ci/prepare-android-build
uses: ./.github/actions/prepare-android-build
with:
sign: false
- name: ci/build-android-beta
env:
TAG: "${{ github.ref_name }}"
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
run: bundle exec fastlane android unsigned
working-directory: ./fastlane
- name: ci/upload-android-unsigned-build
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
path: Mattermost-unsigned.apk
name: Mattermost-unsigned.apk
release:
runs-on: ubuntu-22.04
needs:
- build-ios-unsigned
- build-android-unsigned
steps:
- name: ci/checkout-repo
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0
- uses: ruby/setup-ruby@9669f3ee51dc3f4eda8447ab696b3ab19a90d14b # v1.144.0
with:
ruby-version: "2.7"
- name: release/setup-fastlane-dependencies
run: bundle install
working-directory: ./fastlane
- name: ci/download-artifacts
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
- name: release/create-github-release
env:
GITHUB_TOKEN: "${{ secrets.MM_MOBILE_GITHUB_TOKEN }}"
run: bundle exec fastlane github
working-directory: ./fastlane

1
.gitignore vendored
View file

@ -103,6 +103,7 @@ detox/detox_pixel_*
# Bundle artifact
*.jsbundle
.bundle
#editor-settings
.vscode

View file

@ -110,8 +110,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 473
versionName "2.5.0"
versionCode 476
versionName "2.5.1"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}

View file

@ -39,7 +39,7 @@ class DatabaseHelper {
private fun setDefaultDatabase(context: Context) {
val databaseName = "app.db"
val databasePath = Uri.fromFile(context.filesDir).toString() + "/" + databaseName
defaultDatabase = Database(databasePath, context)
defaultDatabase = Database.getInstance(databasePath, context)
}
internal fun JSONObject.toMap(): Map<String, Any?> = keys().asSequence().associateWith { it ->

View file

@ -57,7 +57,7 @@ fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): D
if (cursor.count == 1) {
cursor.moveToFirst()
val databasePath = cursor.getString(0)
return Database(databasePath, context!!)
return Database.getInstance(databasePath, context!!)
}
}
} catch (e: Exception) {

View file

@ -7,7 +7,6 @@ import {DeviceEventEmitter} from 'react-native';
import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category';
import {markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
import {switchToGlobalThreads} from '@actions/local/thread';
import {getIsCRTEnabled} from '@app/queries/servers/thread';
import {loadCallForChannel} from '@calls/actions/calls';
import {DeepLink, Events, General, Preferences, Screens} from '@constants';
import DatabaseManager from '@database/manager';
@ -20,6 +19,7 @@ import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {getCommonSystemValues, getConfig, getCurrentChannelId, getCurrentTeamId, getCurrentUserId, getLicense, setCurrentChannelId, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {getNthLastChannelFromTeam, getMyTeamById, getTeamByName, queryMyTeams, removeChannelFromTeamHistory} from '@queries/servers/team';
import {getIsCRTEnabled} from '@queries/servers/thread';
import {getCurrentUser} from '@queries/servers/user';
import {dismissAllModalsAndPopToRoot} from '@screens/navigation';
import EphemeralStore from '@store/ephemeral_store';

View file

@ -17,7 +17,7 @@ export const fetchGroupsForAutocomplete = async (serverUrl: string, query: strin
try {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const license = await getLicense(database);
if (!license || !license.IsLicensed) {
if (!license || license.IsLicensed !== 'true') {
return [];
}
@ -40,7 +40,7 @@ export const fetchGroupsByNames = async (serverUrl: string, names: string[], fet
try {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const license = await getLicense(database);
if (!license || !license.IsLicensed) {
if (!license || license.IsLicensed !== 'true') {
return [];
}
@ -70,7 +70,7 @@ export const fetchGroupsForChannel = async (serverUrl: string, channelId: string
try {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const license = await getLicense(database);
if (!license || !license.IsLicensed) {
if (!license || license.IsLicensed !== 'true') {
return {groups: [], groupChannels: []};
}
@ -102,7 +102,7 @@ export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetc
try {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const license = await getLicense(database);
if (!license || !license.IsLicensed) {
if (!license || license.IsLicensed !== 'true') {
return {groups: [], groupTeams: []};
}
@ -133,7 +133,7 @@ export const fetchGroupsForMember = async (serverUrl: string, userId: string, fe
try {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const license = await getLicense(database);
if (!license || !license.IsLicensed) {
if (!license || license.IsLicensed !== 'true') {
return {groups: [], groupMemberships: []};
}

View file

@ -339,7 +339,7 @@ export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: C
const promises = [];
for (const member of memberships) {
const channel = channels.find((c) => c.id === member.channel_id);
if (channel && (channel.total_msg_count - member.msg_count) > 0 && channel.id !== excludeChannelId) {
if (channel && !channel.delete_at && (channel.total_msg_count - member.msg_count) > 0 && channel.id !== excludeChannelId) {
promises.push(fetchPostsForChannel(serverUrl, channel.id));
}
}

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback, useMemo, useState} from 'react';
import {type StyleProp, StyleSheet, useWindowDimensions, View, type ViewStyle} from 'react-native';
import {StyleSheet, useWindowDimensions, View} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import {buildFilePreviewUrl, buildFileThumbnailUrl} from '@actions/remote/file';
@ -59,15 +59,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
imagePreview: {
...StyleSheet.absoluteFillObject,
},
smallImageBorder: {
borderRadius: 5,
},
smallImageOverlay: {
justifyContent: 'center',
alignItems: 'center',
borderRadius: 4,
position: 'absolute',
},
singleSmallImageWrapper: {
height: SMALL_IMAGE_MAX_HEIGHT,
width: SMALL_IMAGE_MAX_WIDTH,
@ -117,56 +108,9 @@ const ImageFile = ({
return props;
};
const imageDimensions = getImageDimensions();
if (imageDimensions && (imageDimensions.height <= SMALL_IMAGE_MAX_HEIGHT || imageDimensions.width <= SMALL_IMAGE_MAX_WIDTH)) {
let wrapperStyle: StyleProp<ViewStyle> = style.fileImageWrapper;
if (isSingleImage) {
wrapperStyle = style.singleSmallImageWrapper;
if (file.width > SMALL_IMAGE_MAX_WIDTH) {
wrapperStyle = [wrapperStyle, {width: '100%'}];
}
}
image = (
<ProgressiveImage
id={file.id!}
forwardRef={forwardRef}
style={{height: file.height, width: file.width}}
tintDefaultSource={!file.localPath && !failed}
onError={handleError}
resizeMode={'contain'}
{...imageProps()}
/>
);
if (failed) {
image = (
<FileIcon
failed={failed}
file={file}
backgroundColor={backgroundColor}
/>
);
}
return (
<View
style={[
wrapperStyle,
style.smallImageBorder,
{
borderColor: changeOpacity(theme.centerChannelColor, 0.4),
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
},
]}
>
{!isSingleImage && <View style={style.boxPlaceholder}/>}
<View style={style.smallImageOverlay}>
{image}
</View>
</View>
);
let imageDimensions = getImageDimensions();
if (isSingleImage && (!imageDimensions || (imageDimensions?.height === 0 && imageDimensions?.width === 0))) {
imageDimensions = style.singleSmallImageWrapper;
}
image = (

View file

@ -1,10 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect, useState} from 'react';
import React, {useEffect, useRef, useState} from 'react';
import {StyleSheet, View} from 'react-native';
import Badge from '@components/badge';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {subscribeAllServers} from '@database/subscription/servers';
import {subscribeMentionsByServer} from '@database/subscription/unreads';
@ -27,9 +29,9 @@ const styles = StyleSheet.create({
},
});
const subscriptions: Map<string, UnreadSubscription> = new Map();
const OtherMentionsBadge = ({channelId}: Props) => {
const currentServerUrl = useServerUrl();
const subscriptions: Map<string, UnreadSubscription> = useRef(new Map()).current;
const [count, setCount] = useState(0);
const updateCount = () => {
@ -50,7 +52,10 @@ const OtherMentionsBadge = ({channelId}: Props) => {
}
}
unreads.mentions = mentions + threadMentionCount;
unreads.mentions = mentions;
if (serverUrl !== currentServerUrl || channelId !== Screens.GLOBAL_THREADS) {
unreads.mentions += threadMentionCount;
}
subscriptions.set(serverUrl, unreads);
updateCount();
}

View file

@ -166,7 +166,7 @@ export default function SendHandler({
const sendCommand = useCallback(async () => {
if (value.trim().startsWith('/call')) {
const {handled, error} = await handleCallsSlashCommand(value.trim(), serverUrl, channelId, currentUserId, intl);
const {handled, error} = await handleCallsSlashCommand(value.trim(), serverUrl, channelId, rootId, currentUserId, intl);
if (handled) {
setSendingMessage(false);
clearDraft();

View file

@ -198,7 +198,7 @@ const PostList = ({
const viewableItemsMap = viewableItems.reduce((acc: Record<string, boolean>, {item, isViewable}) => {
if (isViewable && item.type === 'post') {
acc[`${location}-${item.value.id}`] = true;
acc[`${location}-${item.value.currentPost.id}`] = true;
}
return acc;
}, {});

View file

@ -7,6 +7,7 @@ import FastImage, {type ResizeMode} from 'react-native-fast-image';
import Animated, {interpolate, useAnimatedStyle, useDerivedValue, useSharedValue, withTiming} from 'react-native-reanimated';
import {useTheme} from '@context/theme';
import {emptyFunction} from '@utils/general';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import Thumbnail from './thumbnail';
@ -153,8 +154,9 @@ const ProgressiveImage = ({
<Animated.View
style={[styles.defaultImageContainer, style, containerStyle]}
>
{Boolean(thumbnailUri) &&
<Thumbnail
onError={onError}
onError={emptyFunction}
opacity={defaultOpacity}
source={{uri: thumbnailUri}}
style={[
@ -163,6 +165,7 @@ const ProgressiveImage = ({
]}
tintColor={thumbnailUri ? undefined : theme.centerChannelColor}
/>
}
{image}
</Animated.View>
);

View file

@ -186,11 +186,11 @@ export default class BaseDataOperator {
try {
if (models.length > 0) {
await this.database.write(async (writer) => {
await writer.batch(models);
await writer.batch(...models);
}, description);
}
} catch (e) {
logWarning('batchRecords error ', e as Error);
logWarning('batchRecords error ', description, e as Error);
}
}

View file

@ -16,8 +16,8 @@ import {requestNotifications} from 'react-native-permissions';
import {storeDeviceToken} from '@actions/app/global';
import {markChannelAsViewed} from '@actions/local/channel';
import {updateThread} from '@actions/local/thread';
import {backgroundNotification, openNotification} from '@actions/remote/notifications';
import {markThreadAsRead} from '@actions/remote/thread';
import {Device, Events, Navigation, PushNotification, Screens} from '@constants';
import DatabaseManager from '@database/manager';
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
@ -90,7 +90,12 @@ class PushNotifications {
if (isCRTEnabled && payload.root_id) {
const thread = await getThreadById(database, payload.root_id);
if (thread?.isFollowing) {
markThreadAsRead(serverUrl, payload.team_id, payload.root_id);
const data: Partial<ThreadWithViewedAt> = {
unread_mentions: 0,
unread_replies: 0,
last_viewed_at: Date.now(),
};
updateThread(serverUrl, payload.root_id, data);
}
} else {
markChannelAsViewed(serverUrl, payload.channel_id);

View file

@ -2,11 +2,9 @@
// See LICENSE.txt for license information.
import {Alert, DeviceEventEmitter, Linking, NativeEventEmitter, NativeModules} from 'react-native';
import RNLocalize from 'react-native-localize';
import semver from 'semver';
import {switchToChannelById} from '@actions/remote/channel';
import {autoUpdateTimezone} from '@actions/remote/user';
import LocalConfig from '@assets/config.json';
import {Device, Events, Sso} from '@constants';
import {MIN_REQUIRED_VERSION} from '@constants/supported_server';
@ -14,13 +12,12 @@ import DatabaseManager from '@database/manager';
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
import {getServerCredentials} from '@init/credentials';
import * as analytics from '@managers/analytics';
import {getAllServers, getActiveServerUrl} from '@queries/app/servers';
import {getActiveServerUrl} from '@queries/app/servers';
import {queryTeamDefaultChannel} from '@queries/servers/channel';
import {getCommonSystemValues} from '@queries/servers/system';
import {getTeamChannelHistory} from '@queries/servers/team';
import {setScreensOrientation} from '@screens/navigation';
import {handleDeepLink} from '@utils/deep_link';
import {logError} from '@utils/log';
type LinkingCallbackArg = {url: string};
@ -33,19 +30,6 @@ class GlobalEventHandler {
constructor() {
DeviceEventEmitter.addListener(Events.SERVER_VERSION_CHANGED, this.onServerVersionChanged);
DeviceEventEmitter.addListener(Events.CONFIG_CHANGED, this.onServerConfigChanged);
RNLocalize.addEventListener('change', async () => {
try {
const servers = await getAllServers();
for (const server of servers) {
if (server.url && server.lastActiveAt > 0) {
autoUpdateTimezone(server.url);
}
}
} catch (e) {
logError('Localize change', e);
}
});
splitViewEmitter.addListener('SplitViewChanged', this.onSplitViewChanged);
Linking.addEventListener('url', this.onDeepLink);
}

View file

@ -89,6 +89,13 @@ jest.mock('@queries/servers/thread', () => ({
}));
jest.mock('@calls/alerts');
jest.mock('react-native-navigation', () => ({
Navigation: {
pop: jest.fn(() => Promise.resolve({
catch: jest.fn(),
})),
},
}));
const addFakeCall = (serverUrl: string, channelId: string) => {
const call = {

View file

@ -3,7 +3,6 @@
import {Alert} from 'react-native';
import InCallManager from 'react-native-incall-manager';
import {Navigation} from 'react-native-navigation';
import {forceLogoutIfNecessary} from '@actions/remote/session';
import {updateThreadFollowing} from '@actions/remote/thread';
@ -24,7 +23,7 @@ import {
getCurrentCall,
getChannelsWithCalls,
} from '@calls/state';
import {General, Preferences, Screens} from '@constants';
import {General, Preferences} from '@constants';
import Calls from '@constants/calls';
import DatabaseManager from '@database/manager';
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
@ -34,8 +33,6 @@ import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {getConfig, getLicense} from '@queries/servers/system';
import {getThreadById} from '@queries/servers/thread';
import {getCurrentUser, getUserById} from '@queries/servers/user';
import {dismissAllModalsAndPopToScreen} from '@screens/navigation';
import NavigationStore from '@store/navigation_store';
import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log';
import {displayUsername, getUserIdFromChannelName, isSystemAdmin} from '@utils/user';
@ -211,6 +208,7 @@ export const joinCall = async (
userId: string,
hasMicPermission: boolean,
title?: string,
rootId?: string,
): Promise<{ error?: unknown; data?: string }> => {
// Edge case: calls was disabled when app loaded, and then enabled, but app hasn't
// reconnected its websocket since then (i.e., hasn't called batchLoadCalls yet)
@ -229,7 +227,7 @@ export const joinCall = async (
try {
connection = await newConnection(serverUrl, channelId, () => {
myselfLeftCall();
}, setScreenShareURL, hasMicPermission, title);
}, setScreenShareURL, hasMicPermission, title, rootId);
} catch (error) {
await forceLogoutIfNecessary(serverUrl, error);
return {error};
@ -271,16 +269,6 @@ export const leaveCall = () => {
}
};
export const leaveCallPopCallScreen = async () => {
leaveCall();
// Need to pop the call screen, if it's somewhere in the stack.
if (NavigationStore.getScreensInStack().includes(Screens.CALL)) {
await dismissAllModalsAndPopToScreen(Screens.CALL, 'Call');
Navigation.pop(Screens.CALL).catch(() => null);
}
};
export const muteMyself = () => {
if (connection) {
connection.mute();
@ -430,7 +418,7 @@ export const stopCallRecording = async (serverUrl: string, callId: string) => {
};
// handleCallsSlashCommand will return true if the slash command was handled
export const handleCallsSlashCommand = async (value: string, serverUrl: string, channelId: string, currentUserId: string, intl: IntlShape):
export const handleCallsSlashCommand = async (value: string, serverUrl: string, channelId: string, rootId: string, currentUserId: string, intl: IntlShape):
Promise<{ handled?: boolean; error?: string }> => {
const tokens = value.split(' ');
if (tokens.length < 2 || tokens[0] !== '/call') {
@ -451,15 +439,17 @@ export const handleCallsSlashCommand = async (value: string, serverUrl: string,
};
}
const title = tokens.length > 2 ? tokens.slice(2).join(' ') : undefined;
await leaveAndJoinWithAlert(intl, serverUrl, channelId, title);
await leaveAndJoinWithAlert(intl, serverUrl, channelId, title, rootId);
return {handled: true};
}
case 'join':
await leaveAndJoinWithAlert(intl, serverUrl, channelId);
case 'join': {
const title = tokens.length > 2 ? tokens.slice(2).join(' ') : undefined;
await leaveAndJoinWithAlert(intl, serverUrl, channelId, title, rootId);
return {handled: true};
}
case 'leave':
if (getCurrentCall()?.channelId === channelId) {
await leaveCallPopCallScreen();
await leaveCall();
return {handled: true};
}
return {

View file

@ -3,8 +3,7 @@
import {Alert} from 'react-native';
import {hasMicrophonePermission, joinCall, unmuteMyself} from '@calls/actions';
import {leaveCallPopCallScreen} from '@calls/actions/calls';
import {hasMicrophonePermission, joinCall, leaveCall, unmuteMyself} from '@calls/actions';
import {hasBluetoothPermission} from '@calls/actions/permissions';
import {
getCallsConfig,
@ -73,6 +72,7 @@ export const leaveAndJoinWithAlert = async (
joinServerUrl: string,
joinChannelId: string,
title?: string,
rootId?: string,
) => {
let leaveChannelName = '';
let joinChannelName = '';
@ -133,13 +133,13 @@ export const leaveAndJoinWithAlert = async (
id: 'mobile.leave_and_join_confirmation',
defaultMessage: 'Leave & Join',
}),
onPress: () => doJoinCall(joinServerUrl, joinChannelId, joinChannelIsDMorGM, newCall, intl, title),
onPress: () => doJoinCall(joinServerUrl, joinChannelId, joinChannelIsDMorGM, newCall, intl, title, rootId),
style: 'cancel',
},
],
);
} else {
doJoinCall(joinServerUrl, joinChannelId, joinChannelIsDMorGM, newCall, intl, title);
doJoinCall(joinServerUrl, joinChannelId, joinChannelIsDMorGM, newCall, intl, title, rootId);
}
};
@ -150,6 +150,7 @@ const doJoinCall = async (
newCall: boolean,
intl: IntlShape,
title?: string,
rootId?: string,
) => {
const {formatMessage} = intl;
@ -198,7 +199,7 @@ const doJoinCall = async (
const hasPermission = await hasMicrophonePermission();
setMicPermissionsGranted(hasPermission);
const res = await joinCall(serverUrl, channelId, user.id, hasPermission, title);
const res = await joinCall(serverUrl, channelId, user.id, hasPermission, title, rootId);
if (res.error) {
const seeLogs = formatMessage({id: 'mobile.calls_see_logs', defaultMessage: 'See server logs'});
errorAlert(res.error?.toString() || seeLogs, intl);
@ -265,7 +266,7 @@ export const recordingAlert = (isHost: boolean, intl: IntlShape) => {
defaultMessage: 'Leave',
}),
onPress: async () => {
await leaveCallPopCallScreen();
await leaveCall();
},
style: 'destructive',
},

View file

@ -31,6 +31,7 @@ export async function newConnection(
setScreenShareURL: (url: string) => void,
hasMicPermission: boolean,
title?: string,
rootId?: string,
) {
let peer: RTCPeer | null = null;
let stream: MediaStream;
@ -323,6 +324,7 @@ export async function newConnection(
ws.send('join', {
channelID,
title,
threadID: rootId,
});
}
});

View file

@ -4,7 +4,6 @@
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {useIntl} from 'react-intl';
import {
DeviceEventEmitter,
Keyboard,
type LayoutChangeEvent,
type LayoutRectangle,
@ -40,7 +39,7 @@ import {getHandsRaised, makeCallsTheme, sortParticipants} from '@calls/utils';
import CompassIcon from '@components/compass_icon';
import FormattedText from '@components/formatted_text';
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
import {Calls, Preferences, Screens, WebsocketEvents} from '@constants';
import {Calls, Preferences, Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import DatabaseManager from '@database/manager';
@ -56,7 +55,6 @@ import {
popTopScreen,
setScreensOrientation,
} from '@screens/navigation';
import NavigationStore from '@store/navigation_store';
import {freezeOtherScreens} from '@utils/gallery';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {mergeNavigationOptions} from '@utils/navigation';
@ -504,16 +502,6 @@ const CallScreen = ({
popTopScreen(componentId);
});
useEffect(() => {
const listener = DeviceEventEmitter.addListener(WebsocketEvents.CALLS_CALL_END, ({channelId}) => {
if (channelId === currentCall?.channelId && NavigationStore.getVisibleScreen() === componentId) {
Navigation.pop(componentId);
}
});
return () => listener.remove();
}, []);
useEffect(() => {
const didDismissListener = Navigation.events().registerComponentDidDisappearListener(async ({componentId: screen}) => {
if (componentId === screen) {
@ -550,13 +538,6 @@ const CallScreen = ({
}, []);
if (!currentCall || !myParticipant) {
// Note: this happens because the screen is "rendered", even after the screen has been popped, and the
// currentCall will have already been set to null when those extra renders run. We probably don't ever need
// to pop, but just in case.
if (NavigationStore.getVisibleScreen() === componentId) {
// ignore the error because the call screen has likely already been popped async
Navigation.pop(componentId).catch(() => null);
}
return null;
}

View file

@ -75,6 +75,14 @@ jest.mock('@queries/servers/thread', () => ({
})),
}));
jest.mock('react-native-navigation', () => ({
Navigation: {
pop: jest.fn(() => Promise.resolve({
catch: jest.fn(),
})),
},
}));
const call1: Call = {
participants: {
'user-1': {id: 'user-1', muted: false, raisedHand: 0},

View file

@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import {mosThreshold} from '@mattermost/calls/lib/rtc_monitor';
import {Navigation} from 'react-native-navigation';
import {updateThreadFollowing} from '@actions/remote/thread';
import {needsRecordingAlert} from '@calls/alerts';
@ -27,7 +28,7 @@ import {
DefaultCurrentCall,
type ReactionStreamEmoji,
} from '@calls/types/calls';
import {Calls} from '@constants';
import {Calls, Screens} from '@constants';
import DatabaseManager from '@database/manager';
import {getChannelById} from '@queries/servers/channel';
import {getThreadById} from '@queries/servers/thread';
@ -217,6 +218,10 @@ export const newCurrentCall = (serverUrl: string, channelId: string, myUserId: s
export const myselfLeftCall = () => {
setCurrentCall(null);
// Remove the call screen, and in some situations it needs to be removed twice before actually being removed.
Navigation.pop(Screens.CALL).catch(() => null);
Navigation.pop(Screens.CALL).catch(() => null);
};
export const callStarted = async (serverUrl: string, call: Call) => {

View file

@ -4,6 +4,7 @@
import BottomSheetM, {BottomSheetBackdrop, type BottomSheetBackdropProps, type BottomSheetFooterProps} from '@gorhom/bottom-sheet';
import React, {type ReactNode, useCallback, useEffect, useMemo, useRef} from 'react';
import {DeviceEventEmitter, type Handle, InteractionManager, Keyboard, type StyleProp, View, type ViewStyle} from 'react-native';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
import {Events} from '@constants';
import {useTheme} from '@context/theme';
@ -38,6 +39,7 @@ const PADDING_TOP_TABLET = 8;
export const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
container: {flex: 1},
bottomSheet: {
backgroundColor: theme.centerChannelBg,
borderTopStartRadius: 24,
@ -197,25 +199,27 @@ const BottomSheet = ({
}
return (
<BottomSheetM
ref={sheetRef}
index={initialSnapIndex}
snapPoints={snapPoints}
animateOnMount={true}
backdropComponent={renderBackdrop}
onAnimate={handleAnimationStart}
onChange={handleChange}
animationConfigs={animatedConfig}
handleComponent={Indicator}
style={styles.bottomSheet}
backgroundStyle={bottomSheetBackgroundStyle}
footerComponent={footerComponent}
keyboardBehavior='extend'
keyboardBlurBehavior='restore'
onClose={close}
>
{renderContainerContent()}
</BottomSheetM>
<GestureHandlerRootView style={styles.container}>
<BottomSheetM
ref={sheetRef}
index={initialSnapIndex}
snapPoints={snapPoints}
animateOnMount={true}
backdropComponent={renderBackdrop}
onAnimate={handleAnimationStart}
onChange={handleChange}
animationConfigs={animatedConfig}
handleComponent={Indicator}
style={styles.bottomSheet}
backgroundStyle={bottomSheetBackgroundStyle}
footerComponent={footerComponent}
keyboardBehavior='extend'
keyboardBlurBehavior='restore'
onClose={close}
>
{renderContainerContent()}
</BottomSheetM>
</GestureHandlerRootView>
);
};

View file

@ -11,6 +11,7 @@ import CompassIcon from '@components/compass_icon';
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
import NavigationHeader from '@components/navigation_header';
import {ITEM_HEIGHT} from '@components/option_item';
import OtherMentionsBadge from '@components/other_mentions_badge';
import RoundedHeaderContext from '@components/rounded_header_context';
import {General, Screens} from '@constants';
import {useTheme} from '@context/theme';
@ -23,7 +24,6 @@ import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import OtherMentionsBadge from './other_mentions_badge';
import QuickActions, {MARGIN, SEPARATOR_HEIGHT} from './quick_actions';
import type {HeaderRightButton} from '@components/navigation_header/header';

View file

@ -68,7 +68,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
class ClearAfterModal extends NavigationComponent<Props, State> {
private backListener: NativeEventSubscription | undefined;
constructor(props: Props) {
super(props);
super({...props, componentName: 'ClearAfterModal'});
const options: Options = {
topBar: {

View file

@ -155,7 +155,6 @@ const EditProfile = ({
}
close();
return;
} catch (e) {
resetScreen(e);
}

View file

@ -8,7 +8,9 @@ import {type Edge, SafeAreaView} from 'react-native-safe-area-context';
import {setGlobalThreadsTab} from '@actions/local/systems';
import NavigationHeader from '@components/navigation_header';
import OtherMentionsBadge from '@components/other_mentions_badge';
import RoundedHeaderContext from '@components/rounded_header_context';
import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useIsTablet} from '@hooks/device';
@ -49,6 +51,14 @@ const GlobalThreads = ({componentId, globalThreadsTab}: Props) => {
return {flex: 1, marginTop};
}, [defaultHeight]);
const headerLeftComponent = useMemo(() => {
if (isTablet) {
return undefined;
}
return (<OtherMentionsBadge channelId={Screens.GLOBAL_THREADS}/>);
}, [isTablet]);
useEffect(() => {
mounted.current = true;
return () => {
@ -85,6 +95,7 @@ const GlobalThreads = ({componentId, globalThreadsTab}: Props) => {
defaultMessage: 'Threads',
})
}
leftComponent={headerLeftComponent}
/>
<View style={contextStyle}>
<RoundedHeaderContext/>

View file

@ -9,12 +9,16 @@ import {DeviceEventEmitter, Platform} from 'react-native';
import HWKeyboardEvent from 'react-native-hw-keyboard-event';
import {enableFreeze, enableScreens} from 'react-native-screens';
import {autoUpdateTimezone} from '@actions/remote/user';
import ServerVersion from '@components/server_version';
import {Events, Screens} from '@constants';
import {useTheme} from '@context/theme';
import {useAppState} from '@hooks/device';
import {getAllServers} from '@queries/app/servers';
import {findChannels, popToRoot} from '@screens/navigation';
import NavigationStore from '@store/navigation_store';
import {handleDeepLink} from '@utils/deep_link';
import {logError} from '@utils/log';
import {alertChannelArchived, alertChannelRemove, alertTeamRemove} from '@utils/navigation';
import {notificationError} from '@utils/notification';
@ -40,9 +44,23 @@ type HomeProps = LaunchProps & {
const Tab = createBottomTabNavigator();
const updateTimezoneIfNeeded = async () => {
try {
const servers = await getAllServers();
for (const server of servers) {
if (server.url && server.lastActiveAt > 0) {
autoUpdateTimezone(server.url);
}
}
} catch (e) {
logError('Localize change', e);
}
};
export default function HomeScreen(props: HomeProps) {
const theme = useTheme();
const intl = useIntl();
const appState = useAppState();
useEffect(() => {
const listener = DeviceEventEmitter.addListener(Events.NOTIFICATION_ERROR, (value: 'Team' | 'Channel' | 'Post' | 'Connection') => {
@ -95,6 +113,12 @@ export default function HomeScreen(props: HomeProps) {
};
}, [intl.locale]);
useEffect(() => {
if (appState === 'active') {
updateTimezoneIfNeeded();
}
}, [appState]);
useEffect(() => {
if (props.launchType === 'deeplink') {
const deepLink = props.extra as DeepLinkWithData;

View file

@ -115,7 +115,7 @@ const RecentMentionsScreen = ({appsEnabled, customEmojiNames, mentions, currentT
const viewableItemsMap = viewableItems.reduce((acc: Record<string, boolean>, {item, isViewable}) => {
if (isViewable && item.type === 'post') {
acc[`${Screens.MENTIONS}-${item.value.id}`] = true;
acc[`${Screens.MENTIONS}-${item.value.currentPost.id}`] = true;
}
return acc;
}, {});

View file

@ -110,7 +110,7 @@ function SavedMessages({appsEnabled, posts, currentTimezone, customEmojiNames, i
const viewableItemsMap = viewableItems.reduce((acc: Record<string, boolean>, {item, isViewable}) => {
if (isViewable && item.type === 'post') {
acc[`${Screens.SAVED_MESSAGES}-${item.value.id}`] = true;
acc[`${Screens.SAVED_MESSAGES}-${item.value.currentPost.id}`] = true;
}
return acc;
}, {});

View file

@ -72,7 +72,10 @@ Navigation.setLazyComponentRegistrator((screenName) => {
break;
case Screens.BOTTOM_SHEET:
screen = withServerDatabase(require('@screens/bottom_sheet').default);
break;
Navigation.registerComponent(Screens.BOTTOM_SHEET, () =>
withSafeAreaInsets(withManagedConfig(screen)),
);
return;
case Screens.BROWSE_CHANNELS:
screen = withServerDatabase(require('@screens/browse_channels').default);
break;

View file

@ -87,7 +87,7 @@ function SavedMessages({
const viewableItemsMap = viewableItems.reduce((acc: Record<string, boolean>, {item, isViewable}) => {
if (isViewable && item.type === 'post') {
acc[`${Screens.PINNED_MESSAGES}-${item.value.id}`] = true;
acc[`${Screens.PINNED_MESSAGES}-${item.value.currentPost.id}`] = true;
}
return acc;
}, {});

View file

@ -28,7 +28,8 @@ export function isErrorWithDetails(obj: unknown): obj is {details: Error} {
return (
typeof obj === 'object' &&
obj !== null &&
'details' in obj
'details' in obj &&
typeof obj.details !== 'undefined'
);
}

View file

@ -495,11 +495,14 @@ export const hasWriteStoragePermission = async (intl: IntlShape) => {
},
{applicationName},
);
const text = intl.formatMessage({
id: 'mobile.write_storage_permission_denied_description',
defaultMessage:
'Save files to your device. Open Settings to grant {applicationName} write access to files on this device.',
});
const text = intl.formatMessage(
{
id: 'mobile.write_storage_permission_denied_description',
defaultMessage:
'Save files to your device. Open Settings to grant {applicationName} write access to files on this device.',
},
{applicationName},
);
Alert.alert(title, text, [
{

View file

@ -952,7 +952,11 @@
"settings.save": "Save",
"share_extension.channel_error": "You are not a member of a team on the selected server. Select another server or open Mattermost to join a team.",
"share_extension.channel_label": "Channel",
"share_extension.channels_screen.title": "Select channel",
"share_extension.count_limit": "You can only share {count, number} {count, plural, one {file} other {files}} on this server",
"share_extension.error_screen.description": "There was an error when attempting to share the content to {applicationName}.",
"share_extension.error_screen.label": "An error ocurred",
"share_extension.error_screen.reason": "Reason: {reason}",
"share_extension.file_limit.multiple": "Each file must be less than {size}",
"share_extension.file_limit.single": "File must be less than {size}",
"share_extension.max_resolution": "Image exceeds maximum dimensions of 7680 x 4320 px",
@ -960,7 +964,7 @@
"share_extension.multiple_label": "{count, number} attachments",
"share_extension.server_label": "Server",
"share_extension.servers_screen.title": "Select server",
"share_extension.share_screen.title": "Share to Mattermost",
"share_extension.share_screen.title": "Share to {applicationName}",
"share_extension.upload_disabled": "File uploads are disabled for the selected server",
"share_feedback.button.no": "No, thanks",
"share_feedback.button.yes": "Yes",

View file

@ -11,7 +11,6 @@ module.exports = {
['@babel/plugin-proposal-decorators', {legacy: true}],
['@babel/plugin-transform-flow-strip-types'],
['@babel/plugin-proposal-class-properties', {loose: true}],
['@babel/plugin-proposal-private-property-in-object', {loose: true}],
['module-resolver', {
root: ['.'],
alias: {

3107
detox/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,21 +5,21 @@
"author": "Mattermost, Inc.",
"devDependencies": {
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-transform-modules-commonjs": "7.21.2",
"@babel/plugin-transform-runtime": "7.21.4",
"@babel/preset-env": "7.21.4",
"@babel/plugin-transform-modules-commonjs": "7.22.5",
"@babel/plugin-transform-runtime": "7.22.5",
"@babel/preset-env": "7.22.5",
"@jest/test-sequencer": "29.5.0",
"@types/jest": "29.5.1",
"@types/jest": "29.5.2",
"@types/tough-cookie": "4.0.2",
"@types/uuid": "9.0.1",
"aws-sdk": "2.1361.0",
"axios": "1.3.6",
"@types/uuid": "9.0.2",
"aws-sdk": "2.1398.0",
"axios": "1.4.0",
"axios-cookiejar-support": "4.0.6",
"babel-jest": "29.5.0",
"babel-plugin-module-resolver": "5.0.0",
"client-oauth2": "4.3.3",
"deepmerge": "4.3.1",
"detox": "20.7.0",
"detox": "20.9.1",
"form-data": "4.0.0",
"jest": "29.5.0",
"jest-circus": "29.5.0",
@ -27,15 +27,15 @@
"jest-html-reporters": "3.1.4",
"jest-junit": "16.0.0",
"jest-stare": "2.5.0",
"junit-report-merger": "6.0.1",
"junit-report-merger": "6.0.2",
"moment-timezone": "0.5.43",
"recursive-readdir": "2.2.3",
"sanitize-filename": "1.6.3",
"shelljs": "0.8.5",
"tough-cookie": "4.1.2",
"tough-cookie": "4.1.3",
"ts-jest": "29.1.0",
"tslib": "2.5.0",
"typescript": "5.0.4",
"tslib": "2.5.3",
"typescript": "5.1.3",
"uuid": "9.0.0",
"xml2js": "0.5.0"
},

View file

@ -0,0 +1,18 @@
## You invoke this with --env android.beta on the fastlane command
APP_NAME="Mattermost Beta"
APP_SCHEME=mattermost
AWS_BUCKET_NAME=releases.mattermost.com
AWS_FOLDER_NAME=mattermost-mobile-beta
AWS_REGION=us-east-1
BETA_BUILD=true
BUILD_FOR_RELEASE=true
MAIN_APP_IDENTIFIER=com.mattermost.rnbeta
REPLACE_ASSETS=false
SENTRY_ENABLED=true
SENTRY_ORG=mattermost-mr
SENTRY_PROJECT_ANDROID=mattermost-android-test
SEPARATE_APKS=true
SHOW_ONBOARDING=true
SUPPLY_TRACK=alpha
SUPPLY_PACKAGE_NAME=com.mattermost.rnbeta
SUPPLY_VALIDATE_ONLY=false

12
fastlane/.env.android.pr Normal file
View file

@ -0,0 +1,12 @@
## You invoke this with --env android.pr on the fastlane command
APP_NAME="Mattermost Beta"
APP_SCHEME=mattermost
AWS_BUCKET_NAME=pr-builds.mattermost.com
AWS_FOLDER_NAME=mattermost-mobile
AWS_REGION=us-east-1
BETA_BUILD=true
BUILD_FOR_RELEASE=true
BUILD_PR=true
MAIN_APP_IDENTIFIER=com.mattermost.rnbeta
REPLACE_ASSETS=false
SHOW_ONBOARDING=true

View file

@ -0,0 +1,19 @@
## You invoke this with --env android.release on the fastlane command
APP_NAME=Mattermost
APP_SCHEME=mattermost
AWS_BUCKET_NAME=releases.mattermost.com
AWS_FOLDER_NAME=mattermost-mobile
AWS_REGION=us-east-1
BETA_BUILD=false
BUILD_FOR_RELEASE=true
MAIN_APP_IDENTIFIER=com.mattermost.rn
REPLACE_ASSETS=true
SENTRY_ENABLED=true
SENTRY_ORG=mattermost-mr
SENTRY_PROJECT_ANDROID=mattermost-mobile-android
SEPARATE_APKS=true
SHOW_ONBOARDING=true
SHOW_REVIEW=true
SUPPLY_PACKAGE_NAME=com.mattermost.rn
SUPPLY_TRACK=beta
SUPPLY_VALIDATE_ONLY=false

4
fastlane/.env.default Normal file
View file

@ -0,0 +1,4 @@
## This is loaded by default for all fastlane executions
BABEL_ENV="production"
NODE_ENV="production"
NODE_OPTIONS="--max_old_space_size=12000"

26
fastlane/.env.ios.beta Normal file
View file

@ -0,0 +1,26 @@
## You invoke this with --env ios.beta on the fastlane command
APP_NAME="Mattermost Beta"
APP_SCHEME=mattermost
AWS_BUCKET_NAME=releases.mattermost.com
AWS_FOLDER_NAME=mattermost-mobile-beta
AWS_REGION=us-east-1
BETA_BUILD=true
BUILD_FOR_RELEASE=true
EXTENSION_APP_IDENTIFIER=com.mattermost.rnbeta.MattermostShare
IOS_APP_GROUP=group.com.mattermost.rnbeta
IOS_BUILD_EXPORT_METHOD=app-store
IOS_ICLOUD_CONTAINER=iCloud.com.mattermost.rnbeta
MAIN_APP_IDENTIFIER=com.mattermost.rnbeta
MATCH_APP_IDENTIFIER=com.mattermost.rnbeta.NotificationService,com.mattermost.rnbeta.MattermostShare,com.mattermost.rnbeta
MATCH_READONLY=true
MATCH_SHALLOW_CLONE=true
MATCH_SKIP_DOCS=true
MATCH_TYPE=appstore
NOTIFICATION_SERVICE_IDENTIFIER=com.mattermost.rnbeta.NotificationService
PILOT_SKIP_WAITING_FOR_BUILD_PROCESSING=true
REPLACE_ASSETS=false
SENTRY_ENABLED=true
SENTRY_ORG=mattermost-mr
SENTRY_PROJECT_IOS=mattermost-ios-test
SHOW_ONBOARDING=true
SYNC_PROVISIONING_PROFILES=true

24
fastlane/.env.ios.pr Normal file
View file

@ -0,0 +1,24 @@
## You invoke this with --env ios.pr on the fastlane command
APP_NAME="Mattermost Beta"
APP_SCHEME=mattermost
AWS_BUCKET_NAME=pr-builds.mattermost.com
AWS_FOLDER_NAME=mattermost-mobile
AWS_REGION=us-east-1
BETA_BUILD=true
BUILD_FOR_RELEASE=true
BUILD_PR=true
EXTENSION_APP_IDENTIFIER=com.mattermost.rnbeta.MattermostShare
IOS_APP_GROUP=group.com.mattermost.rnbeta
IOS_BUILD_EXPORT_METHOD=ad-hoc
IOS_ICLOUD_CONTAINER=iCloud.com.mattermost.rnbeta
MAIN_APP_IDENTIFIER=com.mattermost.rnbeta
MATCH_APP_IDENTIFIER=com.mattermost.rnbeta.NotificationService,com.mattermost.rnbeta.MattermostShare,com.mattermost.rnbeta
MATCH_READONLY=true
MATCH_SHALLOW_CLONE=true
MATCH_SKIP_DOCS=true
MATCH_TYPE=adhoc
NOTIFICATION_SERVICE_IDENTIFIER=com.mattermost.rnbeta.NotificationService
PILOT_SKIP_WAITING_FOR_BUILD_PROCESSING=true
REPLACE_ASSETS=false
SHOW_ONBOARDING=true
SYNC_PROVISIONING_PROFILES=true

26
fastlane/.env.ios.release Normal file
View file

@ -0,0 +1,26 @@
## You invoke this with --env ios.release on the fastlane command
APP_NAME=Mattermost
APP_SCHEME=mattermost
AWS_BUCKET_NAME=releases.mattermost.com
AWS_FOLDER_NAME=mattermost-mobile
AWS_REGION=us-east-1
BUILD_FOR_RELEASE=true
EXTENSION_APP_IDENTIFIER=com.mattermost.rn.MattermostShare
IOS_APP_GROUP=group.com.mattermost
IOS_BUILD_EXPORT_METHOD=app-store
IOS_ICLOUD_CONTAINER=iCloud.com.mattermost.rn
MAIN_APP_IDENTIFIER=com.mattermost.rn
MATCH_APP_IDENTIFIER=com.mattermost.rn.NotificationService,com.mattermost.rn.MattermostShare,com.mattermost.rn
MATCH_READONLY=true
MATCH_SHALLOW_CLONE=true
MATCH_SKIP_DOCS=true
MATCH_TYPE=appstore
NOTIFICATION_SERVICE_IDENTIFIER=com.mattermost.rn.NotificationService
PILOT_SKIP_WAITING_FOR_BUILD_PROCESSING=true
REPLACE_ASSETS=true
SENTRY_ENABLED=true
SENTRY_ORG=mattermost-mr
SENTRY_PROJECT_IOS=mattermost-mobile-ios
SHOW_ONBOARDING=true
SHOW_REVIEW=true
SYNC_PROVISIONING_PROFILES=true

View file

@ -0,0 +1,4 @@
## You invoke this with --env ios.simulator on the fastlane command
AWS_BUCKET_NAME=releases.mattermost.com
AWS_FOLDER_NAME=mattermost-mobile-beta
AWS_REGION=us-east-1

View file

@ -14,11 +14,11 @@ before_all do
ENV['SPACESHIP_SKIP_2FA_UPGRADE'] = '1'
is_build_pr = ENV['BUILD_PR'] == 'true'
if is_build_pr && ENV['CIRCLECI'] == 'true'
ENV['BRANCH_TO_BUILD'] = ENV['CIRCLE_BRANCH']
if is_build_pr && ENV['CI'] == 'true'
ENV['BRANCH_TO_BUILD'] = ENV['GITHUB_REF_NAME']
end
if ENV['CIRCLECI'] != 'true'
if ENV['CI'] != 'true'
# Raises an error is git is not clean
if ENV['COMMIT_CHANGES_TO_GIT'] == 'true'
ensure_git_status_clean
@ -46,9 +46,9 @@ before_all do
end
after_all do |lane|
if ENV['CIRCLECI'] != 'true'
if ENV['CI'] != 'true'
if ENV['RESET_GIT_BRANCH'] == 'true'
branch = ENV['BRANCH_TO_BUILD'] || 'master'
branch = ENV['BRANCH_TO_BUILD'] || 'main'
package_id = ENV['MAIN_APP_IDENTIFIER'] || 'com.mattermost.rnbeta'
beta_dir = '../android/app/src/main/java/com/mattermost/rnbeta'
release_dir = "../android/app/src/main/java/#{package_id.gsub '.', '/'}"
@ -68,7 +68,7 @@ after_all do |lane|
sh "git branch -D #{local_branch}"
UI.success("Deleted working branch \"#{local_branch}\"")
if lane.to_s == 'build_pr'
sh 'git checkout master'
sh 'git checkout main'
## Remove the branch for the PR
sh "git branch -D #{branch}"
UI.success("Deleted PR branch \"#{branch}\"")
@ -320,7 +320,7 @@ end
desc 'Create GitHub release'
lane :github do
tag = ENV['CIRCLE_TAG'] || ENV['TAG']
tag = ENV['GITHUB_REF_NAME'] || ENV['TAG']
if tag
version = android_get_version_name(gradle_file: './android/app/build.gradle')
@ -350,8 +350,8 @@ end
platform :ios do
before_all do
if ENV['CIRCLECI'] == 'true'
setup_circle_ci
if ENV['CI'] == 'true'
setup_ci
end
end
@ -415,7 +415,7 @@ platform :ios do
end
lane :update_identifiers do
# Set the name for the app
# Set the name for the app and share extension
app_name = ENV['APP_NAME'] || 'Mattermost Beta'
update_info_plist(
xcodeproj: './ios/Mattermost.xcodeproj',
@ -423,6 +423,12 @@ platform :ios do
display_name: app_name
)
update_info_plist(
xcodeproj: './ios/Mattermost.xcodeproj',
plist_path: 'MattermostShare/Info.plist',
display_name: app_name
)
# Set the notification service extension bundle identifier
notification_bundle_id = ENV['NOTIFICATION_SERVICE_IDENTIFIER'] || 'com.mattermost.rnbeta.NotificationService'
update_app_identifier(
@ -563,20 +569,27 @@ platform :ios do
json = JSON.parse(localization_file)
plist_strings = ''
localized_strings = ''
json.each do |key, value|
if key.match?('mobile.ios.plist')
unless value.nil? || value.empty?
unless value.nil? || value.empty?
if key.match?('mobile.ios.plist')
plist_strings.concat("#{key.split('.').last} = \"#{value}\";\n")
else
value.gsub!('\\', '\\\\\\\\') # How to replace backslash with double backslash https://stackoverflow.com/a/6209532
value.gsub!('"', '\"')
localized_strings.concat("\"#{key}\" = \"#{value}\";\n")
end
end
end
plist_strings.gsub!('{applicationName}', "#{app_name}")
localized_strings.gsub!('{applicationName}', "#{app_name}")
language = item.split('.').first
project_localization_path = "../ios/Mattermost/i18n/#{language}.lproj"
FileUtils.mkdir_p project_localization_path
File.write("#{project_localization_path}/InfoPlist.strings", plist_strings)
File.write("#{project_localization_path}/Localizable.strings", localized_strings)
end
end

View file

@ -2,6 +2,7 @@ source "https://rubygems.org"
gem "fastlane"
gem "nokogiri"
gem "dotenv"
plugins_path = File.join(File.dirname(__FILE__), '.', 'Pluginfile')
eval(File.read(plugins_path), binding) if File.exist?(plugins_path)

View file

@ -8,17 +8,17 @@ GEM
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.752.0)
aws-sdk-core (3.171.0)
aws-partitions (1.777.0)
aws-sdk-core (3.174.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.63.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (1.66.0)
aws-sdk-core (~> 3, >= 3.174.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.121.0)
aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-s3 (1.123.1)
aws-sdk-core (~> 3, >= 3.174.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.2)
@ -36,7 +36,7 @@ GEM
unf (>= 0.0.5, < 1.0.0)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.99.0)
excon (0.100.0)
faraday (1.10.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
@ -65,8 +65,8 @@ GEM
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.212.2)
fastimage (2.2.7)
fastlane (2.213.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@ -90,7 +90,7 @@ GEM
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (~> 2.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0)
@ -111,7 +111,7 @@ GEM
fastlane-plugin-find_replace_string (0.1.0)
fastlane-plugin-versioning_android (0.1.1)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.39.0)
google-apis-androidpublisher_v3 (0.42.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.0)
addressable (~> 2.5, >= 2.5.1)
@ -159,19 +159,19 @@ GEM
memoist (0.16.2)
mini_magick (4.12.0)
mini_mime (1.1.2)
mini_portile2 (2.8.1)
mini_portile2 (2.8.2)
multi_json (1.15.0)
multipart-post (2.0.0)
multipart-post (2.3.0)
nanaimo (0.3.0)
naturally (2.2.1)
nokogiri (1.14.3)
mini_portile2 (~> 2.8.0)
nokogiri (1.15.2)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
optparse (0.1.1)
os (1.1.4)
plist (3.7.0)
public_suffix (5.0.1)
racc (1.6.2)
racc (1.7.0)
rake (13.0.6)
representable (3.2.0)
declarative (< 0.1.0)
@ -222,6 +222,7 @@ PLATFORMS
ruby
DEPENDENCIES
dotenv
fastlane
fastlane-plugin-android_change_package_identifier
fastlane-plugin-android_change_string_app_name

View file

@ -62,6 +62,29 @@
672D98B029F1927F004228D6 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 672D988229F1927F004228D6 /* InfoPlist.strings */; };
672D98B129F1927F004228D6 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 672D988529F1927F004228D6 /* InfoPlist.strings */; };
672D98B229F1927F004228D6 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 672D988529F1927F004228D6 /* InfoPlist.strings */; };
67AE0FC52A0556A500810C56 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67AE0FC32A0556A500810C56 /* Localizable.strings */; };
67FEADD92A0E96DD00DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEADD62A0E96DD00DDF4AE /* Localizable.strings */; };
67FEADDD2A12602A00DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEADDA2A12602A00DDF4AE /* Localizable.strings */; };
67FEADE12A12603600DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEADDE2A12603600DDF4AE /* Localizable.strings */; };
67FEADE52A12603F00DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEADE22A12603F00DDF4AE /* Localizable.strings */; };
67FEADE92A12604900DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEADE62A12604900DDF4AE /* Localizable.strings */; };
67FEADED2A12608900DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEADEA2A12608900DDF4AE /* Localizable.strings */; };
67FEADF12A12609300DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEADEE2A12609300DDF4AE /* Localizable.strings */; };
67FEADF52A1260A900DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEADF22A1260A900DDF4AE /* Localizable.strings */; };
67FEADF92A1260B200DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEADF62A1260B200DDF4AE /* Localizable.strings */; };
67FEAE012A1260C200DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEADFE2A1260C200DDF4AE /* Localizable.strings */; };
67FEAE052A1260CA00DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEAE022A1260C900DDF4AE /* Localizable.strings */; };
67FEAE092A12611B00DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEAE062A12611B00DDF4AE /* Localizable.strings */; };
67FEAE0D2A12612500DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEAE0A2A12612500DDF4AE /* Localizable.strings */; };
67FEAE112A12613000DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEAE0E2A12613000DDF4AE /* Localizable.strings */; };
67FEAE152A12614F00DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEAE122A12614F00DDF4AE /* Localizable.strings */; };
67FEAE192A12615900DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEAE162A12615900DDF4AE /* Localizable.strings */; };
67FEAE1D2A12616500DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEAE1A2A12616500DDF4AE /* Localizable.strings */; };
67FEAE212A12616F00DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEAE1E2A12616F00DDF4AE /* Localizable.strings */; };
67FEAE252A12618000DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEAE222A12618000DDF4AE /* Localizable.strings */; };
67FEAE292A1261A000DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEAE262A1261A000DDF4AE /* Localizable.strings */; };
67FEAE2A2A1261EA00DDF4AE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 67FEADFA2A1260B900DDF4AE /* Localizable.strings */; };
67FEAE2C2A127C3600DDF4AE /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67FEAE2B2A127C3600DDF4AE /* NumberFormatter.swift */; };
6C9B1EFD6561083917AF06CF /* libPods-Mattermost.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DEEFB3ED6175724A2653247 /* libPods-Mattermost.a */; };
7F0F4B0A24BA173900E14C60 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7F0F4B0924BA173900E14C60 /* LaunchScreen.storyboard */; };
7F151D3E221B062700FAD8F3 /* RuntimeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F151D3D221B062700FAD8F3 /* RuntimeUtils.swift */; };
@ -246,6 +269,29 @@
672D988029F1927F004228D6 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = InfoPlist.strings; sourceTree = "<group>"; };
672D988329F1927F004228D6 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = InfoPlist.strings; sourceTree = "<group>"; };
672D988629F1927F004228D6 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = InfoPlist.strings; sourceTree = "<group>"; };
67AE0FC42A0556A500810C56 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = Localizable.strings; sourceTree = "<group>"; };
67FEADD72A0E96DD00DDF4AE /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Localizable.strings; sourceTree = "<group>"; };
67FEADDB2A12602A00DDF4AE /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = Localizable.strings; sourceTree = "<group>"; };
67FEADDF2A12603600DDF4AE /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = Localizable.strings; sourceTree = "<group>"; };
67FEADE32A12603F00DDF4AE /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = Localizable.strings; sourceTree = "<group>"; };
67FEADE72A12604900DDF4AE /* en_AU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en_AU; path = Localizable.strings; sourceTree = "<group>"; };
67FEADEB2A12608900DDF4AE /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = Localizable.strings; sourceTree = "<group>"; };
67FEADEF2A12609300DDF4AE /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = Localizable.strings; sourceTree = "<group>"; };
67FEADF32A1260A900DDF4AE /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = Localizable.strings; sourceTree = "<group>"; };
67FEADF72A1260B200DDF4AE /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = Localizable.strings; sourceTree = "<group>"; };
67FEADFB2A1260B900DDF4AE /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = Localizable.strings; sourceTree = "<group>"; };
67FEADFF2A1260C200DDF4AE /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = Localizable.strings; sourceTree = "<group>"; };
67FEAE032A1260C900DDF4AE /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = Localizable.strings; sourceTree = "<group>"; };
67FEAE072A12611B00DDF4AE /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = Localizable.strings; sourceTree = "<group>"; };
67FEAE0B2A12612500DDF4AE /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = Localizable.strings; sourceTree = "<group>"; };
67FEAE0F2A12613000DDF4AE /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = Localizable.strings; sourceTree = "<group>"; };
67FEAE132A12614F00DDF4AE /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = Localizable.strings; sourceTree = "<group>"; };
67FEAE172A12615900DDF4AE /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = Localizable.strings; sourceTree = "<group>"; };
67FEAE1B2A12616500DDF4AE /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = Localizable.strings; sourceTree = "<group>"; };
67FEAE1F2A12616F00DDF4AE /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = Localizable.strings; sourceTree = "<group>"; };
67FEAE232A12618000DDF4AE /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = Localizable.strings; sourceTree = "<group>"; };
67FEAE272A1261A000DDF4AE /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = Localizable.strings; sourceTree = "<group>"; };
67FEAE2B2A127C3600DDF4AE /* NumberFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumberFormatter.swift; sourceTree = "<group>"; };
6BAF8296411D4657B5A0E8F8 /* libRNReactNativeDocViewer.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNReactNativeDocViewer.a; sourceTree = "<group>"; };
7F0F4B0924BA173900E14C60 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = SplashScreenResource/LaunchScreen.storyboard; sourceTree = "<group>"; };
7F151D3D221B062700FAD8F3 /* RuntimeUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RuntimeUtils.swift; path = Mattermost/RuntimeUtils.swift; sourceTree = "<group>"; };
@ -512,6 +558,7 @@
672D984529F1927E004228D6 /* de.lproj */ = {
isa = PBXGroup;
children = (
67FEADE22A12603F00DDF4AE /* Localizable.strings */,
672D984629F1927E004228D6 /* InfoPlist.strings */,
);
name = de.lproj;
@ -521,6 +568,7 @@
672D984829F1927E004228D6 /* ja.lproj */ = {
isa = PBXGroup;
children = (
67FEADFE2A1260C200DDF4AE /* Localizable.strings */,
672D984929F1927E004228D6 /* InfoPlist.strings */,
);
name = ja.lproj;
@ -530,6 +578,7 @@
672D984B29F1927E004228D6 /* fa.lproj */ = {
isa = PBXGroup;
children = (
67FEADEE2A12609300DDF4AE /* Localizable.strings */,
672D984C29F1927E004228D6 /* InfoPlist.strings */,
);
name = fa.lproj;
@ -539,6 +588,7 @@
672D984E29F1927F004228D6 /* pl.lproj */ = {
isa = PBXGroup;
children = (
67FEAE0A2A12612500DDF4AE /* Localizable.strings */,
672D984F29F1927F004228D6 /* InfoPlist.strings */,
);
name = pl.lproj;
@ -548,6 +598,7 @@
672D985129F1927F004228D6 /* ar.lproj */ = {
isa = PBXGroup;
children = (
67FEADDA2A12602A00DDF4AE /* Localizable.strings */,
672D985229F1927F004228D6 /* InfoPlist.strings */,
);
name = ar.lproj;
@ -557,6 +608,7 @@
672D985429F1927F004228D6 /* sv.lproj */ = {
isa = PBXGroup;
children = (
67FEAE122A12614F00DDF4AE /* Localizable.strings */,
672D985529F1927F004228D6 /* InfoPlist.strings */,
);
name = sv.lproj;
@ -566,6 +618,7 @@
672D985729F1927F004228D6 /* ru.lproj */ = {
isa = PBXGroup;
children = (
67AE0FC32A0556A500810C56 /* Localizable.strings */,
672D985829F1927F004228D6 /* InfoPlist.strings */,
);
name = ru.lproj;
@ -575,6 +628,7 @@
672D985A29F1927F004228D6 /* ro.lproj */ = {
isa = PBXGroup;
children = (
67FEAE262A1261A000DDF4AE /* Localizable.strings */,
672D985B29F1927F004228D6 /* InfoPlist.strings */,
);
name = ro.lproj;
@ -584,6 +638,7 @@
672D985D29F1927F004228D6 /* bg.lproj */ = {
isa = PBXGroup;
children = (
67FEADDE2A12603600DDF4AE /* Localizable.strings */,
672D985E29F1927F004228D6 /* InfoPlist.strings */,
);
name = bg.lproj;
@ -593,6 +648,7 @@
672D986029F1927F004228D6 /* tr.lproj */ = {
isa = PBXGroup;
children = (
67FEAE162A12615900DDF4AE /* Localizable.strings */,
672D986129F1927F004228D6 /* InfoPlist.strings */,
);
name = tr.lproj;
@ -602,6 +658,7 @@
672D986329F1927F004228D6 /* zh-TW.lproj */ = {
isa = PBXGroup;
children = (
67FEAE222A12618000DDF4AE /* Localizable.strings */,
672D986429F1927F004228D6 /* InfoPlist.strings */,
);
name = "zh-TW.lproj";
@ -611,6 +668,7 @@
672D986629F1927F004228D6 /* en.lproj */ = {
isa = PBXGroup;
children = (
67FEADD62A0E96DD00DDF4AE /* Localizable.strings */,
672D986729F1927F004228D6 /* InfoPlist.strings */,
);
name = en.lproj;
@ -620,6 +678,7 @@
672D986929F1927F004228D6 /* en_AU.lproj */ = {
isa = PBXGroup;
children = (
67FEADE62A12604900DDF4AE /* Localizable.strings */,
672D986A29F1927F004228D6 /* InfoPlist.strings */,
);
name = en_AU.lproj;
@ -629,6 +688,7 @@
672D986C29F1927F004228D6 /* hu.lproj */ = {
isa = PBXGroup;
children = (
67FEADF62A1260B200DDF4AE /* Localizable.strings */,
672D986D29F1927F004228D6 /* InfoPlist.strings */,
);
name = hu.lproj;
@ -638,6 +698,7 @@
672D986F29F1927F004228D6 /* fr.lproj */ = {
isa = PBXGroup;
children = (
67FEADF22A1260A900DDF4AE /* Localizable.strings */,
672D987029F1927F004228D6 /* InfoPlist.strings */,
);
name = fr.lproj;
@ -647,6 +708,7 @@
672D987229F1927F004228D6 /* ko.lproj */ = {
isa = PBXGroup;
children = (
67FEAE022A1260C900DDF4AE /* Localizable.strings */,
672D987329F1927F004228D6 /* InfoPlist.strings */,
);
name = ko.lproj;
@ -656,6 +718,7 @@
672D987529F1927F004228D6 /* zh-CN.lproj */ = {
isa = PBXGroup;
children = (
67FEAE1E2A12616F00DDF4AE /* Localizable.strings */,
672D987629F1927F004228D6 /* InfoPlist.strings */,
);
name = "zh-CN.lproj";
@ -665,6 +728,7 @@
672D987829F1927F004228D6 /* es.lproj */ = {
isa = PBXGroup;
children = (
67FEADEA2A12608900DDF4AE /* Localizable.strings */,
672D987929F1927F004228D6 /* InfoPlist.strings */,
);
name = es.lproj;
@ -674,6 +738,7 @@
672D987B29F1927F004228D6 /* it.lproj */ = {
isa = PBXGroup;
children = (
67FEADFA2A1260B900DDF4AE /* Localizable.strings */,
672D987C29F1927F004228D6 /* InfoPlist.strings */,
);
name = it.lproj;
@ -683,6 +748,7 @@
672D987E29F1927F004228D6 /* nl.lproj */ = {
isa = PBXGroup;
children = (
67FEAE062A12611B00DDF4AE /* Localizable.strings */,
672D987F29F1927F004228D6 /* InfoPlist.strings */,
);
name = nl.lproj;
@ -692,6 +758,7 @@
672D988129F1927F004228D6 /* pt-BR.lproj */ = {
isa = PBXGroup;
children = (
67FEAE0E2A12613000DDF4AE /* Localizable.strings */,
672D988229F1927F004228D6 /* InfoPlist.strings */,
);
name = "pt-BR.lproj";
@ -701,6 +768,7 @@
672D988429F1927F004228D6 /* uk.lproj */ = {
isa = PBXGroup;
children = (
67FEAE1A2A12616500DDF4AE /* Localizable.strings */,
672D988529F1927F004228D6 /* InfoPlist.strings */,
);
name = uk.lproj;
@ -784,6 +852,7 @@
7F7E9F432864E6AF0064BFAF /* Extensions */ = {
isa = PBXGroup;
children = (
67FEAE2B2A127C3600DDF4AE /* NumberFormatter.swift */,
7F7E9F442864E6C60064BFAF /* Color.swift */,
7F7E9F452864E6C60064BFAF /* View.swift */,
7F93AAAD287725660047B89F /* Int64.swift */,
@ -1197,8 +1266,17 @@
672D98AA29F1927F004228D6 /* InfoPlist.strings in Resources */,
672D989029F1927F004228D6 /* InfoPlist.strings in Resources */,
672D989A29F1927F004228D6 /* InfoPlist.strings in Resources */,
67FEADF52A1260A900DDF4AE /* Localizable.strings in Resources */,
67FEADD92A0E96DD00DDF4AE /* Localizable.strings in Resources */,
67FEAE092A12611B00DDF4AE /* Localizable.strings in Resources */,
67FEADE52A12603F00DDF4AE /* Localizable.strings in Resources */,
7FD4825E2864DC5800A5B18B /* Metropolis-Regular.ttf in Resources */,
67FEAE252A12618000DDF4AE /* Localizable.strings in Resources */,
67FEADF92A1260B200DDF4AE /* Localizable.strings in Resources */,
67FEAE1D2A12616500DDF4AE /* Localizable.strings in Resources */,
7FD4825F2864DC5800A5B18B /* Metropolis-SemiBold.ttf in Resources */,
67FEAE0D2A12612500DDF4AE /* Localizable.strings in Resources */,
67FEADE92A12604900DDF4AE /* Localizable.strings in Resources */,
7FD482602864DC5800A5B18B /* OpenSans-Bold.ttf in Resources */,
7FD482612864DC5800A5B18B /* OpenSans-BoldItalic.ttf in Resources */,
672D988829F1927F004228D6 /* InfoPlist.strings in Resources */,
@ -1207,21 +1285,34 @@
672D989429F1927F004228D6 /* InfoPlist.strings in Resources */,
672D98A029F1927F004228D6 /* InfoPlist.strings in Resources */,
672D989229F1927F004228D6 /* InfoPlist.strings in Resources */,
67FEAE292A1261A000DDF4AE /* Localizable.strings in Resources */,
7FD482632864DC5900A5B18B /* OpenSans-ExtraBoldItalic.ttf in Resources */,
672D988E29F1927F004228D6 /* InfoPlist.strings in Resources */,
7FD482642864DC5900A5B18B /* OpenSans-Italic.ttf in Resources */,
672D989C29F1927F004228D6 /* InfoPlist.strings in Resources */,
672D98A229F1927F004228D6 /* InfoPlist.strings in Resources */,
672D988C29F1927F004228D6 /* InfoPlist.strings in Resources */,
67FEADF12A12609300DDF4AE /* Localizable.strings in Resources */,
67FEAE112A12613000DDF4AE /* Localizable.strings in Resources */,
67AE0FC52A0556A500810C56 /* Localizable.strings in Resources */,
67FEAE152A12614F00DDF4AE /* Localizable.strings in Resources */,
7FD482652864DC5900A5B18B /* OpenSans-Light.ttf in Resources */,
672D98AE29F1927F004228D6 /* InfoPlist.strings in Resources */,
67FEAE052A1260CA00DDF4AE /* Localizable.strings in Resources */,
67FEAE2A2A1261EA00DDF4AE /* Localizable.strings in Resources */,
67FEAE012A1260C200DDF4AE /* Localizable.strings in Resources */,
7FD482662864DC5900A5B18B /* OpenSans-LightItalic.ttf in Resources */,
7FD482672864DC5900A5B18B /* OpenSans-Regular.ttf in Resources */,
672D98B229F1927F004228D6 /* InfoPlist.strings in Resources */,
672D989829F1927F004228D6 /* InfoPlist.strings in Resources */,
67FEADE12A12603600DDF4AE /* Localizable.strings in Resources */,
67FEAE192A12615900DDF4AE /* Localizable.strings in Resources */,
67FEADDD2A12602A00DDF4AE /* Localizable.strings in Resources */,
7FD482682864DC5900A5B18B /* OpenSans-SemiBold.ttf in Resources */,
672D98AC29F1927F004228D6 /* InfoPlist.strings in Resources */,
67FEAE212A12616F00DDF4AE /* Localizable.strings in Resources */,
7FD482692864DC5900A5B18B /* OpenSans-SemiBoldItalic.ttf in Resources */,
67FEADED2A12608900DDF4AE /* Localizable.strings in Resources */,
7FD4825C2864DC4200A5B18B /* compass-icons.ttf in Resources */,
7FD482292864D69700A5B18B /* Assets.xcassets in Resources */,
672D98A829F1927F004228D6 /* InfoPlist.strings in Resources */,
@ -1413,6 +1504,7 @@
7F1EBFCE286F2AE200824AF1 /* MultipleAttachmentView.swift in Sources */,
7F7E9F792864E8160064BFAF /* SearchBarView.swift in Sources */,
7FB50D222856E1AB0035ACB5 /* ShareUIView.swift in Sources */,
67FEAE2C2A127C3600DDF4AE /* NumberFormatter.swift in Sources */,
7F93AAA828761FA10047B89F /* NoMembershipView.swift in Sources */,
7F7E9F772864E8160064BFAF /* LinkPreview.swift in Sources */,
7F7E9F782864E8160064BFAF /* OptionView.swift in Sources */,
@ -1634,6 +1726,182 @@
name = InfoPlist.strings;
sourceTree = "<group>";
};
67AE0FC32A0556A500810C56 /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67AE0FC42A0556A500810C56 /* ru */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEADD62A0E96DD00DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEADD72A0E96DD00DDF4AE /* en */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEADDA2A12602A00DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEADDB2A12602A00DDF4AE /* ar */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEADDE2A12603600DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEADDF2A12603600DDF4AE /* bg */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEADE22A12603F00DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEADE32A12603F00DDF4AE /* de */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEADE62A12604900DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEADE72A12604900DDF4AE /* en_AU */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEADEA2A12608900DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEADEB2A12608900DDF4AE /* es */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEADEE2A12609300DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEADEF2A12609300DDF4AE /* fa */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEADF22A1260A900DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEADF32A1260A900DDF4AE /* fr */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEADF62A1260B200DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEADF72A1260B200DDF4AE /* hu */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEADFA2A1260B900DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEADFB2A1260B900DDF4AE /* it */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEADFE2A1260C200DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEADFF2A1260C200DDF4AE /* ja */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEAE022A1260C900DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEAE032A1260C900DDF4AE /* ko */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEAE062A12611B00DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEAE072A12611B00DDF4AE /* nl */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEAE0A2A12612500DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEAE0B2A12612500DDF4AE /* pl */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEAE0E2A12613000DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEAE0F2A12613000DDF4AE /* pt-BR */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEAE122A12614F00DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEAE132A12614F00DDF4AE /* sv */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEAE162A12615900DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEAE172A12615900DDF4AE /* tr */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEAE1A2A12616500DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEAE1B2A12616500DDF4AE /* uk */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEAE1E2A12616F00DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEAE1F2A12616F00DDF4AE /* zh-CN */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEAE222A12618000DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEAE232A12618000DDF4AE /* zh-TW */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
67FEAE262A1261A000DDF4AE /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
67FEAE272A1261A000DDF4AE /* ro */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
7FC5698A28563FDB000B0905 /* MainInterface.storyboard */ = {
isa = PBXVariantGroup;
children = (
@ -1655,7 +1923,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 473;
CURRENT_PROJECT_VERSION = 476;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
@ -1699,7 +1967,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 473;
CURRENT_PROJECT_VERSION = 476;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
@ -1842,14 +2110,12 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 473;
CURRENT_PROJECT_VERSION = 476;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = MattermostShare/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = MattermostShare;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -1893,14 +2159,12 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 473;
CURRENT_PROJECT_VERSION = 476;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = MattermostShare/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = MattermostShare;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -2066,7 +2330,7 @@
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/getsentry/sentry-cocoa.git";
requirement = {
branch = 8.4.0;
branch = 8.7.3;
kind = branch;
};
};

View file

@ -14,8 +14,8 @@
"package": "Sentry",
"repositoryURL": "https://github.com/getsentry/sentry-cocoa.git",
"state": {
"branch": "8.4.0",
"revision": "92a6472efc750a4e18bdee21c204942ab0bc4dcd",
"branch": "8.7.3",
"revision": "9cf7d2e514af1600cc2b3c5592e2848c6c5a76d6",
"version": null
}
},

View file

@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.5.0</string>
<string>2.5.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@ -37,7 +37,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>473</string>
<string>476</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View file

@ -22,10 +22,7 @@ extension Int64 {
let i = floor(log(size) / log(k))
// Format number with thousands separator and everything below 1 giga with no decimal places.
let numberFormatter = NumberFormatter()
numberFormatter.maximumFractionDigits = i < 3 ? 0 : 1
numberFormatter.numberStyle = .decimal
let numberFormatter = i < 3 ? NumberFormatter.noFractionDigitsDecimalFormatter : NumberFormatter.oneFractionDigitDecimalFormatter
let numberString = numberFormatter.string(from: NSNumber(value: size / pow(k, i))) ?? "Unknown"
let suffix = suffixes[Int(i)]
return "\(numberString) \(suffix)"

View file

@ -0,0 +1,25 @@
//
// NumberFormatter.swift
// MattermostShare
//
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
//
import Foundation
extension NumberFormatter {
static let noFractionDigitsDecimalFormatter: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.maximumFractionDigits = 0
numberFormatter.numberStyle = .decimal
return numberFormatter
}()
static let oneFractionDigitDecimalFormatter: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.maximumFractionDigits = 1
numberFormatter.numberStyle = .decimal
return numberFormatter
}()
}

View file

@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>2.5.0</string>
<string>2.5.1</string>
<key>CFBundleVersion</key>
<string>473</string>
<string>476</string>
<key>UIAppFonts</key>
<array>
<string>OpenSans-Bold.ttf</string>
@ -77,5 +77,7 @@
<string>$(SENTRY_DSN_IOS)</string>
<key>SENTRY_ENABLED</key>
<string>$(SENTRY_ENABLED)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.</string>
</dict>
</plist>

View file

@ -15,16 +15,30 @@ struct AttachmentsView: View {
var error: String? {
if let server = shareViewModel.server {
if server.uploadsDisabled {
return "File uploads are disabled for the selected server"
return NSLocalizedString("share_extension.upload_disabled",
value: "File uploads are disabled for the selected server",
comment: ""
)
}
let sizeError = attachments.contains { $0.sizeError(server: server)}
let resolutionError = attachments.contains { $0.resolutionError(server: server)}
if sizeError && attachments.count == 1 {
return "File must be less than \(server.maxFileSize.formattedFileSize)"
return NSLocalizedString("share_extension.file_limit.single",
value: "File must be less than {size}",
comment: ""
)
.replacingOccurrences(of: "{size}", with: server.maxFileSize.formattedFileSize)
} else if sizeError {
return "Each file must be less than \(server.maxFileSize.formattedFileSize)"
return NSLocalizedString("share_extension.file_limit.multiple",
value: "Each file must be less than {size}",
comment: ""
)
.replacingOccurrences(of: "{size}", with: server.maxFileSize.formattedFileSize)
} else if resolutionError {
return "Image exceeds maximum dimensions of 7680 x 4320 px"
return NSLocalizedString("share_extension.max_resolution",
value: "Image exceeds maximum dimensions of 7680 x 4320 px",
comment: ""
)
}
}

View file

@ -65,10 +65,16 @@ struct MultipleAttachmentView: View {
}
if (attachments.count > 1) {
Text("\(attachments.count) attachments")
.foregroundColor(Color.theme.centerChannelColor.opacity(0.64))
.font(Font.custom("OpenSans", size: 12))
.padding(.leading, 20)
Text(
NSLocalizedString("share_extension.multiple_label",
value: "{count, number} attachments",
comment: ""
)
.replacingOccurrences(of: "{count, number}", with: "\(attachments.count)")
)
.foregroundColor(Color.theme.centerChannelColor.opacity(0.64))
.font(Font.custom("OpenSans", size: 12))
.padding(.leading, 20)
}
}
}

View file

@ -16,9 +16,12 @@ struct ChannelListView: View {
SearchBarView()
if shareViewModel.search.isEmpty {
HStack {
Text("RECENT")
.font(Font.custom("OpenSans-SemiBold", size: 12))
.foregroundColor(Color.theme.centerChannelColor.opacity(0.64))
Text(
NSLocalizedString("mobile.channel_list.recent", value: "Recent", comment: "")
.uppercased()
)
.font(Font.custom("OpenSans-SemiBold", size: 12))
.foregroundColor(Color.theme.centerChannelColor.opacity(0.64))
Spacer()
}
.padding(.top, 20)

View file

@ -16,8 +16,10 @@ struct SearchBarView: View {
HStack {
TextField("", text: $shareViewModel.search)
.placeholder(when: shareViewModel.search.isEmpty) {
Text("Find channels...")
.foregroundColor(Color.theme.centerChannelColor.opacity(0.64))
Text(
NSLocalizedString("channel_list.find_channels", value: "Find channels...", comment: "")
)
.foregroundColor(Color.theme.centerChannelColor.opacity(0.64))
}
.padding(.leading, 40)
.padding(.trailing, 30)
@ -59,9 +61,11 @@ struct SearchBarView: View {
if isEditing {
Button(action: dismissKeyboard) {
Text("Cancel")
.foregroundColor(Color.theme.centerChannelColor)
.font(Font.custom("OpenSans", size: 14))
Text(
NSLocalizedString("mobile.post.cancel", value: "Cancel", comment: "")
)
.foregroundColor(Color.theme.centerChannelColor)
.font(Font.custom("OpenSans", size: 14))
}
.transition(.move(edge: .trailing))
.animation(.linear(duration: 0.15))

View file

@ -41,8 +41,8 @@ struct ContentView: View {
VStack (spacing: 0) {
if shareViewModel.allServers.count > 1 {
OptionView(
navigationTitle: "Select server",
label: "Server",
navigationTitle: NSLocalizedString("share_extension.servers_screen.title", value: "Select server", comment: ""),
label: NSLocalizedString("share_extension.server_label", value: "Server", comment: ""),
value: shareViewModel.server!.displayName
) {
ServerListView()
@ -51,8 +51,8 @@ struct ContentView: View {
}
if hasChannels {
OptionView(
navigationTitle: "Select channel",
label: "Channel",
navigationTitle: NSLocalizedString("share_extension.channels_screen.title", value: "Select channel", comment: ""),
label: NSLocalizedString("share_extension.channel_label", value: "Channel", comment: ""),
value: "\(shareViewModel.channel!.displayName) \(shareViewModel.channel!.formattedTeamName)"
) {
ChannelListView()
@ -66,10 +66,16 @@ struct ContentView: View {
.padding(.bottom, 10)
if hasChannels {
FloatingTextField(placeholderText: "Enter a message (optional)", text: $message)
FloatingTextField(
placeholderText: NSLocalizedString("share_extension.message", value: "Enter a message (optional)", comment: ""),
text: $message
)
} else {
ErrorLabelView(
error: "You are not a member of a team on the selected server. Select another server or open Mattermost to join a team."
error: NSLocalizedString("share_extension.channel_error",
value: "You are not a member of a team on the selected server. Select another server or open Mattermost to join a team.",
comment: ""
)
)
}

View file

@ -53,9 +53,7 @@ struct FloatingTextField: View {
}
func formatLength(_ value: Int64) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 0
let formatter = NumberFormatter.noFractionDigitsDecimalFormatter
let number = NSNumber(value: value)
return formatter.string(from: number)!
}
@ -64,7 +62,12 @@ struct FloatingTextField: View {
VStack (alignment: .leading) {
if (error) {
ErrorLabelView(
error: "Message must be less than \(formatLength(shareViewModel.server!.maxMessageLength)) characters"
error: NSLocalizedString("mobile.message_length.message",
value: "Your current message is too long. Current character count: {count}/{max}",
comment: ""
)
.replacingOccurrences(of: "{count}", with: formatLength(Int64(text.count)))
.replacingOccurrences(of: "{max}", with: formatLength(shareViewModel.server!.maxMessageLength))
)
}
ZStack(alignment: .topLeading) {

View file

@ -22,21 +22,40 @@ struct ErrorSharingView: View {
NotificationCenter.default.post(name: Notification.Name("submit"), object: nil, userInfo: nil)
}
} else {
Text("An error ocurred")
.font(Font.custom("Metropolis-SemiBold", size: 20))
.foregroundColor(Color.theme.centerChannelColor)
Text("There was an error when attempting to share the content to Mattermost.")
.font(Font.custom("OpenSans", size: 16))
Text(
NSLocalizedString("share_extension.error_screen.label", value: "An error ocurred", comment: "")
)
.font(Font.custom("Metropolis-SemiBold", size: 20))
.foregroundColor(Color.theme.centerChannelColor)
Text(
NSLocalizedString("share_extension.error_screen.description",
value: "There was an error when attempting to share the content to {applicationName}.",
comment: ""
)
.replacingOccurrences(
of: "{applicationName}",
with: Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? "Mattermost Beta"
)
)
.font(Font.custom("OpenSans", size: 16))
.foregroundColor(Color.theme.centerChannelColor.opacity(0.72))
Text("Reason: \(error)")
.font(Font.custom("OpenSans", size: 12))
.foregroundColor(Color.theme.centerChannelColor.opacity(0.60))
Text(
NSLocalizedString("share_extension.error_screen.reason",
value: "Reason: {reason}",
comment: ""
)
.replacingOccurrences(of: "{reason}", with: error)
)
.font(Font.custom("OpenSans", size: 12))
.foregroundColor(Color.theme.centerChannelColor.opacity(0.60))
Button {
retrying = true
} label: {
Text("Try again")
.font(Font.custom("OpenSans", size: 16))
.foregroundColor(Color.theme.buttonColor)
Text(
NSLocalizedString("mobile.post.failed_retry", value: "Try again", comment: "")
)
.font(Font.custom("OpenSans", size: 16))
.foregroundColor(Color.theme.buttonColor)
}
.buttonStyle(.borderedProminent)
.tint(Color.theme.buttonBg)

View file

@ -11,12 +11,22 @@ import SwiftUI
struct NoMembershipView: View {
var body: some View {
VStack (spacing: 8) {
Text("Not a member of any team yet")
.font(Font.custom("Metropolis-SemiBold", size: 20))
.foregroundColor(Color.theme.centerChannelColor)
Text("To share content, you'll need to be a member of a team on a Mattermost server.")
.font(Font.custom("OpenSans", size: 16))
.foregroundColor(Color.theme.centerChannelColor.opacity(0.72))
Text(
NSLocalizedString("extension.no_memberships.title",
value: "Not a member of any team yet",
comment: ""
)
)
.font(Font.custom("Metropolis-SemiBold", size: 20))
.foregroundColor(Color.theme.centerChannelColor)
Text(
NSLocalizedString("extension.no_memberships.description",
value: "To share content, you'll need to be a member of a team on a Mattermost server.",
comment: ""
)
)
.font(Font.custom("OpenSans", size: 16))
.foregroundColor(Color.theme.centerChannelColor.opacity(0.72))
}
.padding(.horizontal, 12)
}

View file

@ -11,12 +11,22 @@ import SwiftUI
struct NoServersView: View {
var body: some View {
VStack (spacing: 8) {
Text("Not connected to any servers")
.font(Font.custom("Metropolis-SemiBold", size: 20))
.foregroundColor(Color.theme.centerChannelColor)
Text("To share content, you'll need to be logged in to a Mattermost server.")
.font(Font.custom("OpenSans", size: 16))
.foregroundColor(Color.theme.centerChannelColor.opacity(0.72))
Text(
NSLocalizedString("extension.no_servers.title",
value: "Not connected to any servers",
comment: ""
)
)
.font(Font.custom("Metropolis-SemiBold", size: 20))
.foregroundColor(Color.theme.centerChannelColor)
Text(
NSLocalizedString("extension.no_servers.description",
value: "To share content, you'll need to be logged in to a Mattermost server.",
comment: ""
)
)
.font(Font.custom("OpenSans", size: 16))
.foregroundColor(Color.theme.centerChannelColor.opacity(0.72))
}
.padding(.horizontal, 12)
}

View file

@ -61,7 +61,17 @@ struct InitialView: View {
}
}
.accentColor(.white)
.navigationBarTitle("Share to Mattermost", displayMode: .inline)
.navigationBarTitle(
NSLocalizedString("share_extension.share_screen.title",
value: "Share to {applicationName}",
comment: ""
)
.replacingOccurrences(
of: "{applicationName}",
with: Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? "Mattermost Beta"
),
displayMode: .inline
)
.navigationBarItems(
leading: CancelButton(attachments: attachments),
trailing: PostButton(

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