Add OTA updates support for `testflight` channel (#3291)
* some progress another adjustment, testing another adjustment, testing fix again fix again set default runtime version fix test this script test this script test this script add build numbers to the deployment url clean give script access to build number add `useBuildNumberEnv` without a bump new line fix missing async add channel name to deployment url add updates check on launch for testflight users ver bump init updates on launch for native add `testflight` as default in build submit add is_testflight check * disable inline predictions to prevent ios composer jank * temp bump * Revert "temp bump" This reverts commit 44c51134a35d817c73edb1e635495597c95117b3. * adjustments version bump adjust fixes test * cleanup and finalize drop check down to every 15 minutes adjustments change to 15 mins use jq to get version if necessary rm test on push figured it out remove nightly testflight releases test again again again again again AGAIN ONCE MORE test again again again again again AGAIN test again again again again again AGAIN test again again again again again test again again again again test again again again test again again test again test test test run deploy if necessary run deploy if necessary run deploy if necessary run deploy if necessary run deploy if necessary remove test message fix environment oops cleanup merge in changes * remove unnecessary `workflow_call` * remove changes that have been merged into main now * finalize android update git ignore rm test stuff from the bundle action remove test message test message fix test message test message few android fixes few android fixes fix jq add a test message fix slack webhook create android deployments test 2 create android deployments add `testflight-android` profile to eas.json more cleanup some more cleanup simplify some logic remove unnecessary channel rename to `useOTAUpdates` * rm test portionzio/stable
parent
02b2ab4f1f
commit
73df7e53b3
|
@ -59,7 +59,7 @@ jobs:
|
||||||
echo "$json" > google-services.json
|
echo "$json" > google-services.json
|
||||||
|
|
||||||
- name: 🏗️ EAS Build
|
- name: 🏗️ EAS Build
|
||||||
run: yarn use-build-number eas build -p android --profile production --local --output build.aab --non-interactive
|
run: yarn use-build-number-with-bump eas build -p android --profile production --local --output build.aab --non-interactive
|
||||||
|
|
||||||
- name: 🚀 Deploy
|
- name: 🚀 Deploy
|
||||||
run: eas submit -p android --non-interactive --path build.aab
|
run: eas submit -p android --non-interactive --path build.aab
|
||||||
|
|
|
@ -2,14 +2,13 @@
|
||||||
name: Build and Submit iOS
|
name: Build and Submit iOS
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
|
||||||
- cron: '0 5 * * *'
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
profile:
|
profile:
|
||||||
type: choice
|
type: choice
|
||||||
description: Build profile to use
|
description: Build profile to use
|
||||||
options:
|
options:
|
||||||
|
- testflight
|
||||||
- production
|
- production
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
@ -69,7 +68,7 @@ jobs:
|
||||||
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
|
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
|
||||||
|
|
||||||
- name: 🏗️ EAS Build
|
- name: 🏗️ EAS Build
|
||||||
run: yarn use-build-number eas build -p ios --profile production --local --output build.ipa --non-interactive
|
run: yarn use-build-number-with-bump eas build -p ios --profile ${{ inputs.profile || 'testflight' }} --local --output build.ipa --non-interactive
|
||||||
|
|
||||||
- name: 🚀 Deploy
|
- name: 🚀 Deploy
|
||||||
run: eas submit -p ios --non-interactive --path build.ipa
|
run: eas submit -p ios --non-interactive --path build.ipa
|
||||||
|
|
|
@ -4,6 +4,12 @@ name: Bundle and Deploy EAS Update
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
|
channel:
|
||||||
|
type: choice
|
||||||
|
description: Deployment channel to use
|
||||||
|
options:
|
||||||
|
- testflight
|
||||||
|
- production
|
||||||
runtimeVersion:
|
runtimeVersion:
|
||||||
type: string
|
type: string
|
||||||
description: Runtime version (in x.x.x format) that this update is for
|
description: Runtime version (in x.x.x format) that this update is for
|
||||||
|
@ -13,13 +19,48 @@ jobs:
|
||||||
bundleDeploy:
|
bundleDeploy:
|
||||||
name: Bundle and Deploy EAS Update
|
name: Bundle and Deploy EAS Update
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
fingerprint-diff: ${{ steps.fingerprint.outputs.fingerprint-diff }}
|
||||||
steps:
|
steps:
|
||||||
|
- name: Check for EXPO_TOKEN
|
||||||
|
run: >
|
||||||
|
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
|
||||||
|
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate the version if one is supplied. This should generally happen if the update is for a production client
|
||||||
- name: 🧐 Validate version
|
- name: 🧐 Validate version
|
||||||
|
if: ${{ inputs.runtimeVersion }}
|
||||||
run: |
|
run: |
|
||||||
[[ "${{ github.event.inputs.runtimeVersion }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1
|
if [ -z "${{ inputs.runtimeVersion }}" ]; then
|
||||||
|
[[ "${{ inputs.runtimeVersion }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: ⬇️ Checkout
|
- name: ⬇️ Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 100
|
||||||
|
|
||||||
|
- name: ⬇️ Fetch commits from base branch
|
||||||
|
run: git fetch origin main:main --depth 100
|
||||||
|
|
||||||
|
# This should get the current production release's commit's hash to see if the update is compatible
|
||||||
|
- name: 🕵️ Get the base commit
|
||||||
|
id: base-commit
|
||||||
|
run: |
|
||||||
|
if [ -z "${{ inputs.channel == 'production' }}" ]; then
|
||||||
|
echo base-commit=$(git show-ref -s ${{ inputs.runtimeVersion }}) >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo base-commit=$(git log -n 1 --skip 1 main --pretty=format:'%H') >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: ✓ Make sure we found a base commit
|
||||||
|
run: |
|
||||||
|
if [ -z "${{ steps.base-commit.outputs.base-commit }}" ]; then
|
||||||
|
echo "Could not find a base commit for this release. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: 🔧 Setup Node
|
- name: 🔧 Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
@ -30,13 +71,167 @@ jobs:
|
||||||
- name: ⚙️ Install Dependencies
|
- name: ⚙️ Install Dependencies
|
||||||
run: yarn install
|
run: yarn install
|
||||||
|
|
||||||
- name: 🪛 Install jq
|
# Run the fingerprint
|
||||||
uses: dcarbone/install-jq-action@v2
|
- name: 📷 Check fingerprint
|
||||||
|
id: fingerprint
|
||||||
|
uses: expo/expo-github-action/fingerprint@main
|
||||||
|
with:
|
||||||
|
previous-git-commit: ${{ steps.base-commit.outputs.base-commit }}
|
||||||
|
|
||||||
|
- name: 👀 Debug fingerprint
|
||||||
|
run: |
|
||||||
|
echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}"
|
||||||
|
echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}"
|
||||||
|
|
||||||
|
- name: 🔨 Setup EAS
|
||||||
|
uses: expo/expo-github-action@v8
|
||||||
|
if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }}
|
||||||
|
with:
|
||||||
|
expo-version: latest
|
||||||
|
eas-version: latest
|
||||||
|
token: ${{ secrets.EXPO_TOKEN }}
|
||||||
|
|
||||||
- name: ⛏️ Setup Expo
|
- name: ⛏️ Setup Expo
|
||||||
|
if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }}
|
||||||
run: yarn global add eas-cli-local-build-plugin
|
run: yarn global add eas-cli-local-build-plugin
|
||||||
|
|
||||||
|
- name: 🪛 Setup jq
|
||||||
|
if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }}
|
||||||
|
uses: dcarbone/install-jq-action@v2
|
||||||
|
|
||||||
- name: 🔤 Compile Translations
|
- name: 🔤 Compile Translations
|
||||||
|
if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }}
|
||||||
|
run: yarn intl:build
|
||||||
|
|
||||||
|
- name: ✏️ Write environment variables
|
||||||
|
if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }}
|
||||||
|
run: |
|
||||||
|
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
|
||||||
|
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||||
|
echo "$json" > google-services.json
|
||||||
|
|
||||||
|
- name: 🏗️ Create Bundle
|
||||||
|
if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }}
|
||||||
|
run: EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export
|
||||||
|
|
||||||
|
- name: 📦 Package Bundle and 🚀 Deploy
|
||||||
|
if: ${{ steps.fingerprint.outputs.fingerprint-diff == '[]' }}
|
||||||
|
run: yarn use-build-number bash scripts/bundleUpdate.sh
|
||||||
|
env:
|
||||||
|
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
|
||||||
|
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
|
||||||
|
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
|
||||||
|
|
||||||
|
# GitHub actions are horrible so let's just copy paste this in
|
||||||
|
buildIfNecessaryIOS:
|
||||||
|
name: Build and Submit iOS
|
||||||
|
runs-on: macos-14
|
||||||
|
needs: [bundleDeploy]
|
||||||
|
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
|
||||||
|
# available here
|
||||||
|
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.fingerprint-diff != '[]' }}
|
||||||
|
steps:
|
||||||
|
- name: Check for EXPO_TOKEN
|
||||||
|
run: >
|
||||||
|
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
|
||||||
|
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: ⬇️ Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 🔧 Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: .nvmrc
|
||||||
|
cache: yarn
|
||||||
|
|
||||||
|
- name: 🔨 Setup EAS
|
||||||
|
uses: expo/expo-github-action@v8
|
||||||
|
with:
|
||||||
|
expo-version: latest
|
||||||
|
eas-version: latest
|
||||||
|
token: ${{ secrets.EXPO_TOKEN }}
|
||||||
|
|
||||||
|
- name: ⛏️ Setup EAS local builds
|
||||||
|
run: yarn global add eas-cli-local-build-plugin
|
||||||
|
|
||||||
|
- name: ⚙️ Install dependencies
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: ☕️ Setup Cocoapods
|
||||||
|
uses: maxim-lobanov/setup-cocoapods@v1
|
||||||
|
with:
|
||||||
|
version: 1.14.3
|
||||||
|
|
||||||
|
- name: 💾 Cache Pods
|
||||||
|
uses: actions/cache@v3
|
||||||
|
id: pods-cache
|
||||||
|
with:
|
||||||
|
path: ./ios/Pods
|
||||||
|
# We'll use the yarn.lock for our hash since we don't yet have a Podfile.lock. Pod versions will not
|
||||||
|
# change unless the yarn version changes as well.
|
||||||
|
key: ${{ runner.os }}-pods-${{ hashFiles('yarn.lock') }}
|
||||||
|
|
||||||
|
- name: 🔤 Compile translations
|
||||||
|
run: yarn intl:build
|
||||||
|
|
||||||
|
- name: ✏️ Write environment variables
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||||
|
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
|
||||||
|
|
||||||
|
- name: 🏗️ EAS Build
|
||||||
|
run: yarn use-build-number-with-bump eas build -p ios --profile testflight --local --output build.ipa --non-interactive
|
||||||
|
|
||||||
|
- name: 🚀 Deploy
|
||||||
|
run: eas submit -p ios --non-interactive --path build.ipa
|
||||||
|
|
||||||
|
buildIfNecessaryAndroid:
|
||||||
|
name: Build and Submit Android
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [ bundleDeploy ]
|
||||||
|
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
|
||||||
|
# available here
|
||||||
|
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.fingerprint-diff != '[]' }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check for EXPO_TOKEN
|
||||||
|
run: >
|
||||||
|
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
|
||||||
|
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: ⬇️ Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 🔧 Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: .nvmrc
|
||||||
|
cache: yarn
|
||||||
|
|
||||||
|
- name: 🔨 Setup EAS
|
||||||
|
uses: expo/expo-github-action@v8
|
||||||
|
with:
|
||||||
|
expo-version: latest
|
||||||
|
eas-version: latest
|
||||||
|
token: ${{ secrets.EXPO_TOKEN }}
|
||||||
|
|
||||||
|
- name: ⛏️ Setup EAS local builds
|
||||||
|
run: yarn global add eas-cli-local-build-plugin
|
||||||
|
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: ⚙️ Install dependencies
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: 🔤 Compile translations
|
||||||
run: yarn intl:build
|
run: yarn intl:build
|
||||||
|
|
||||||
- name: ✏️ Write environment variables
|
- name: ✏️ Write environment variables
|
||||||
|
@ -45,11 +240,31 @@ jobs:
|
||||||
echo "${{ secrets.ENV_TOKEN }}" > .env
|
echo "${{ secrets.ENV_TOKEN }}" > .env
|
||||||
echo "$json" > google-services.json
|
echo "$json" > google-services.json
|
||||||
|
|
||||||
- name: 🏗️ Create Bundle
|
- name: 🏗️ EAS Build
|
||||||
run: yarn export
|
run: yarn use-build-number-with-bump eas build -p android --profile testflight-android --local --output build.apk --non-interactive
|
||||||
|
|
||||||
- name: 📦 Package Bundle and 🚀 Deploy
|
- name: ⏰ Get a timestamp
|
||||||
run: yarn make-deploy-bundle
|
id: timestamp
|
||||||
|
uses: nanzm/get-time-action@master
|
||||||
|
with:
|
||||||
|
format: 'MM-DD-HH-mm-ss'
|
||||||
|
|
||||||
|
- name: 🚀 Upload Artifact
|
||||||
|
id: upload-artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
retention-days: 30
|
||||||
|
compression-level: 0
|
||||||
|
name: build-${{ steps.timestamp.outputs.time }}.apk
|
||||||
|
path: build.apk
|
||||||
|
|
||||||
|
- name: 🔔 Notify Slack
|
||||||
|
uses: slackapi/slack-github-action@v1.25.0
|
||||||
|
with:
|
||||||
|
payload: |
|
||||||
|
{
|
||||||
|
"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact.outputs.artifact-url }}"
|
||||||
|
}
|
||||||
env:
|
env:
|
||||||
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
|
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
|
||||||
RUNTIME_VERSION: ${{ github.event.inputs.runtimeVersion }}
|
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
||||||
|
|
|
@ -18,7 +18,6 @@ xcuserdata
|
||||||
*.moved-aside
|
*.moved-aside
|
||||||
DerivedData
|
DerivedData
|
||||||
*.hmap
|
*.hmap
|
||||||
*.ipa
|
|
||||||
*.xcuserstate
|
*.xcuserstate
|
||||||
|
|
||||||
# Android/IntelliJ
|
# Android/IntelliJ
|
||||||
|
@ -110,3 +109,8 @@ google-services.json
|
||||||
# i18n
|
# i18n
|
||||||
src/locale/locales/_build/
|
src/locale/locales/_build/
|
||||||
src/locale/locales/**/*.js
|
src/locale/locales/**/*.js
|
||||||
|
|
||||||
|
# local builds
|
||||||
|
*.apk
|
||||||
|
*.aab
|
||||||
|
*.ipa
|
||||||
|
|
|
@ -41,6 +41,9 @@ module.exports = function (config) {
|
||||||
: process.env.BSKY_IOS_BUILD_NUMBER
|
: process.env.BSKY_IOS_BUILD_NUMBER
|
||||||
|
|
||||||
const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
|
const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
|
||||||
|
const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
|
||||||
|
|
||||||
|
const UPDATES_CHANNEL = IS_TESTFLIGHT ? 'testflight' : 'production'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
expo: {
|
expo: {
|
||||||
|
@ -122,10 +125,20 @@ module.exports = function (config) {
|
||||||
favicon: './assets/favicon.png',
|
favicon: './assets/favicon.png',
|
||||||
},
|
},
|
||||||
updates: {
|
updates: {
|
||||||
enabled: true,
|
url: 'https://updates.bsky.app/manifest',
|
||||||
fallbackToCacheTimeout: 1000,
|
// TODO Eventually we want to enable this for all environments, but for now it will only be used for
|
||||||
url: 'https://u.expo.dev/55bd077a-d905-4184-9c7f-94789ba0f302',
|
// TestFlight builds
|
||||||
|
enabled: IS_TESTFLIGHT,
|
||||||
|
fallbackToCacheTimeout: 30000,
|
||||||
|
codeSigningCertificate: './code-signing/certificate.pem',
|
||||||
|
codeSigningMetadata: {
|
||||||
|
keyid: 'main',
|
||||||
|
alg: 'rsa-v1_5-sha256',
|
||||||
|
},
|
||||||
|
checkAutomatically: 'NEVER',
|
||||||
|
channel: UPDATES_CHANNEL,
|
||||||
},
|
},
|
||||||
|
assetBundlePatterns: ['**/*'],
|
||||||
plugins: [
|
plugins: [
|
||||||
'expo-localization',
|
'expo-localization',
|
||||||
Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
|
Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
|
||||||
|
@ -145,12 +158,6 @@ module.exports = function (config) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
|
||||||
'expo-updates',
|
|
||||||
{
|
|
||||||
username: 'blueskysocial',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
'expo-notifications',
|
'expo-notifications',
|
||||||
{
|
{
|
||||||
|
|
34
eas.json
34
eas.json
|
@ -16,14 +16,20 @@
|
||||||
"ios": {
|
"ios": {
|
||||||
"simulator": true,
|
"simulator": true,
|
||||||
"resourceClass": "large"
|
"resourceClass": "large"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"EXPO_PUBLIC_ENV": "production"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"preview": {
|
"preview": {
|
||||||
"extends": "base",
|
"extends": "base",
|
||||||
"distribution": "internal",
|
"distribution": "internal",
|
||||||
"channel": "preview",
|
"channel": "production",
|
||||||
"ios": {
|
"ios": {
|
||||||
"resourceClass": "large"
|
"resourceClass": "large"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"EXPO_PUBLIC_ENV": "production"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
|
@ -35,9 +41,12 @@
|
||||||
"android": {
|
"android": {
|
||||||
"autoIncrement": true
|
"autoIncrement": true
|
||||||
},
|
},
|
||||||
"channel": "production"
|
"channel": "production",
|
||||||
|
"env": {
|
||||||
|
"EXPO_PUBLIC_ENV": "production"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"github": {
|
"testflight": {
|
||||||
"extends": "base",
|
"extends": "base",
|
||||||
"ios": {
|
"ios": {
|
||||||
"autoIncrement": true
|
"autoIncrement": true
|
||||||
|
@ -45,7 +54,24 @@
|
||||||
"android": {
|
"android": {
|
||||||
"autoIncrement": true
|
"autoIncrement": true
|
||||||
},
|
},
|
||||||
"channel": "production"
|
"channel": "testflight",
|
||||||
|
"env": {
|
||||||
|
"EXPO_PUBLIC_ENV": "testflight"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"testflight-android": {
|
||||||
|
"extends": "base",
|
||||||
|
"distribution": "internal",
|
||||||
|
"ios": {
|
||||||
|
"autoIncrement": true
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"autoIncrement": true
|
||||||
|
},
|
||||||
|
"channel": "testflight",
|
||||||
|
"env": {
|
||||||
|
"EXPO_PUBLIC_ENV": "testflight"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"submit": {
|
"submit": {
|
||||||
|
|
14
package.json
14
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "bsky.app",
|
"name": "bsky.app",
|
||||||
"version": "1.75.0",
|
"version": "1.76.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
@ -14,11 +14,12 @@
|
||||||
"ios": "expo run:ios",
|
"ios": "expo run:ios",
|
||||||
"web": "expo start --web",
|
"web": "expo start --web",
|
||||||
"use-build-number": "./scripts/useBuildNumberEnv.sh",
|
"use-build-number": "./scripts/useBuildNumberEnv.sh",
|
||||||
|
"use-build-number-with-bump": "./scripts/useBuildNumberEnvWithBump.sh",
|
||||||
"build-web": "expo export:web && node ./scripts/post-web-build.js && cp -v ./web-build/static/js/*.* ./bskyweb/static/js/",
|
"build-web": "expo export:web && node ./scripts/post-web-build.js && cp -v ./web-build/static/js/*.* ./bskyweb/static/js/",
|
||||||
"build-all": "yarn intl:build && yarn use-build-number eas build --platform all",
|
"build-all": "yarn intl:build && yarn use-build-number-with-bump eas build --platform all",
|
||||||
"build-ios": "yarn use-build-number eas build -p ios",
|
"build-ios": "yarn use-build-number-with-bump eas build -p ios",
|
||||||
"build-android": "yarn use-build-number eas build -p android",
|
"build-android": "yarn use-build-number-with-bump eas build -p android",
|
||||||
"build": "yarn use-build-number eas build",
|
"build": "yarn use-build-number-with-bump eas build",
|
||||||
"start": "expo start --dev-client",
|
"start": "expo start --dev-client",
|
||||||
"start:prod": "expo start --dev-client --no-dev --minify",
|
"start:prod": "expo start --dev-client --no-dev --minify",
|
||||||
"clean-cache": "rm -rf node_modules/.cache/babel-loader/*",
|
"clean-cache": "rm -rf node_modules/.cache/babel-loader/*",
|
||||||
|
@ -43,8 +44,7 @@
|
||||||
"intl:compile": "lingui compile",
|
"intl:compile": "lingui compile",
|
||||||
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
|
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
|
||||||
"update-extensions": "bash scripts/updateExtensions.sh",
|
"update-extensions": "bash scripts/updateExtensions.sh",
|
||||||
"export": "npx expo export",
|
"export": "npx expo export"
|
||||||
"make-deploy-bundle": "bash scripts/bundleUpdate.sh"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.12.2",
|
"@atproto/api": "^0.12.2",
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
diff --git a/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift b/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift
|
||||||
|
index 189a5f5..8d5b8e6 100644
|
||||||
|
--- a/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift
|
||||||
|
+++ b/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift
|
||||||
|
@@ -68,13 +68,20 @@ public final class NewUpdate: Update {
|
||||||
|
processedAssets.append(asset)
|
||||||
|
}
|
||||||
|
|
||||||
|
+ // Instead of relying on various hacks to get the correct format for the specific
|
||||||
|
+ // platform on the backend, we can just add this little patch..
|
||||||
|
+ let dateFormatter = DateFormatter()
|
||||||
|
+ dateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||||
|
+ dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
|
||||||
|
+ let date = dateFormatter.date(from:commitTime) ?? RCTConvert.nsDate(commitTime)!
|
||||||
|
+
|
||||||
|
return Update(
|
||||||
|
manifest: manifest,
|
||||||
|
config: config,
|
||||||
|
database: database,
|
||||||
|
updateId: uuid,
|
||||||
|
scopeKey: config.scopeKey,
|
||||||
|
- commitTime: RCTConvert.nsDate(commitTime),
|
||||||
|
+ commitTime: date,
|
||||||
|
runtimeVersion: runtimeVersion,
|
||||||
|
keep: true,
|
||||||
|
status: UpdateStatus.StatusPending,
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Expo-Updates Patch
|
||||||
|
|
||||||
|
This is a small patch to convert timestamp formats that are returned from the backend. Instead of relying on the
|
||||||
|
backend to return the correct format for a specific format (the format required on Android is not the same as on iOS)
|
||||||
|
we can just add this conversion in.
|
||||||
|
|
||||||
|
Don't remove unless we make changes on the backend to support both platforms.
|
|
@ -9,10 +9,13 @@ rm -rf bundle.tar.gz
|
||||||
echo "Creating tarball..."
|
echo "Creating tarball..."
|
||||||
node scripts/bundleUpdate.js
|
node scripts/bundleUpdate.js
|
||||||
|
|
||||||
cd bundleTempDir || exit
|
if [ -z "$RUNTIME_VERSION" ]; then
|
||||||
|
RUNTIME_VERSION=$(cat package.json | jq '.version' -r)
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd bundleTempDir || exit
|
||||||
BUNDLE_VERSION=$(date +%s)
|
BUNDLE_VERSION=$(date +%s)
|
||||||
DEPLOYMENT_URL="https://updates.bsky.app/v1/upload?runtime-version=$RUNTIME_VERSION&bundle-version=$BUNDLE_VERSION"
|
DEPLOYMENT_URL="https://updates.bsky.app/v1/upload?runtime-version=$RUNTIME_VERSION&bundle-version=$BUNDLE_VERSION&channel=$CHANNEL_NAME&ios-build-number=$BSKY_IOS_BUILD_NUMBER&android-build-number=$BSKY_ANDROID_VERSION_CODE"
|
||||||
|
|
||||||
tar czvf bundle.tar.gz ./*
|
tar czvf bundle.tar.gz ./*
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
outputIos=$(eas build:version:get -p ios)
|
outputIos=$(eas build:version:get -p ios)
|
||||||
outputAndroid=$(eas build:version:get -p android)
|
outputAndroid=$(eas build:version:get -p android)
|
||||||
currentIosVersion=${outputIos#*buildNumber - }
|
BSKY_IOS_BUILD_NUMBER=${outputIos#*buildNumber - }
|
||||||
currentAndroidVersion=${outputAndroid#*versionCode - }
|
BSKY_ANDROID_VERSION_CODE=${outputAndroid#*versionCode - }
|
||||||
|
|
||||||
BSKY_IOS_BUILD_NUMBER=$((currentIosVersion+1))
|
|
||||||
BSKY_ANDROID_VERSION_CODE=$((currentAndroidVersion+1))
|
|
||||||
|
|
||||||
bash -c "BSKY_IOS_BUILD_NUMBER=$BSKY_IOS_BUILD_NUMBER BSKY_ANDROID_VERSION_CODE=$BSKY_ANDROID_VERSION_CODE $*"
|
bash -c "BSKY_IOS_BUILD_NUMBER=$BSKY_IOS_BUILD_NUMBER BSKY_ANDROID_VERSION_CODE=$BSKY_ANDROID_VERSION_CODE $*"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/bash
|
||||||
|
outputIos=$(eas build:version:get -p ios)
|
||||||
|
outputAndroid=$(eas build:version:get -p android)
|
||||||
|
currentIosVersion=${outputIos#*buildNumber - }
|
||||||
|
currentAndroidVersion=${outputAndroid#*versionCode - }
|
||||||
|
|
||||||
|
BSKY_IOS_BUILD_NUMBER=$((currentIosVersion+1))
|
||||||
|
BSKY_ANDROID_VERSION_CODE=$((currentAndroidVersion+1))
|
||||||
|
|
||||||
|
bash -c "BSKY_IOS_BUILD_NUMBER=$BSKY_IOS_BUILD_NUMBER BSKY_ANDROID_VERSION_CODE=$BSKY_ANDROID_VERSION_CODE $*"
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {init as initPersistedState} from '#/state/persisted'
|
||||||
import * as persisted from '#/state/persisted'
|
import * as persisted from '#/state/persisted'
|
||||||
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
|
||||||
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
|
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
|
||||||
|
import {useOTAUpdates} from 'lib/hooks/useOTAUpdates'
|
||||||
import * as notifications from 'lib/notifications/notifications'
|
import * as notifications from 'lib/notifications/notifications'
|
||||||
import {
|
import {
|
||||||
asyncStoragePersister,
|
asyncStoragePersister,
|
||||||
|
@ -60,6 +61,7 @@ function InnerApp() {
|
||||||
const theme = useColorModeTheme()
|
const theme = useColorModeTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
useIntentHandler()
|
useIntentHandler()
|
||||||
|
useOTAUpdates()
|
||||||
|
|
||||||
// init
|
// init
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
import {AppBskyLabelerDefs} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {AppBskyLabelerDefs} from '@atproto/api'
|
|
||||||
|
|
||||||
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
|
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
|
||||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||||
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import {Button, useButtonContext} from '#/components/Button'
|
import {Button, useButtonContext} from '#/components/Button'
|
||||||
import {Divider} from '#/components/Divider'
|
import {Divider} from '#/components/Divider'
|
||||||
import * as LabelingServiceCard from '#/components/LabelingServiceCard'
|
import * as LabelingServiceCard from '#/components/LabelingServiceCard'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
import {ReportDialogProps} from './types'
|
import {ReportDialogProps} from './types'
|
||||||
|
|
||||||
export function SelectLabelerView({
|
export function SelectLabelerView({
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
import {AppBskyLabelerDefs} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {AppBskyLabelerDefs} from '@atproto/api'
|
|
||||||
|
|
||||||
import {useReportOptions, ReportOption} from '#/lib/moderation/useReportOptions'
|
import {ReportOption, useReportOptions} from '#/lib/moderation/useReportOptions'
|
||||||
import {DMCA_LINK} from '#/components/ReportDialog/const'
|
|
||||||
import {Link} from '#/components/Link'
|
import {Link} from '#/components/Link'
|
||||||
|
import {DMCA_LINK} from '#/components/ReportDialog/const'
|
||||||
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
|
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
|
||||||
|
|
||||||
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ButtonIcon,
|
ButtonIcon,
|
||||||
|
@ -19,11 +18,11 @@ import {
|
||||||
} from '#/components/Button'
|
} from '#/components/Button'
|
||||||
import {Divider} from '#/components/Divider'
|
import {Divider} from '#/components/Divider'
|
||||||
import {
|
import {
|
||||||
ChevronRight_Stroke2_Corner0_Rounded as ChevronRight,
|
|
||||||
ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft,
|
ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft,
|
||||||
|
ChevronRight_Stroke2_Corner0_Rounded as ChevronRight,
|
||||||
} from '#/components/icons/Chevron'
|
} from '#/components/icons/Chevron'
|
||||||
import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from '#/components/icons/SquareArrowTopRight'
|
import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from '#/components/icons/SquareArrowTopRight'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
import {ReportDialogProps} from './types'
|
import {ReportDialogProps} from './types'
|
||||||
|
|
||||||
export function SelectReportOptionView({
|
export function SelectReportOptionView({
|
||||||
|
|
|
@ -1,22 +1,21 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {InterpretedLabelValueDefinition, LabelPreference} from '@atproto/api'
|
import {InterpretedLabelValueDefinition, LabelPreference} from '@atproto/api'
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
|
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
|
||||||
|
import {useLabelBehaviorDescription} from '#/lib/moderation/useLabelBehaviorDescription'
|
||||||
|
import {getLabelStrings} from '#/lib/moderation/useLabelInfo'
|
||||||
import {
|
import {
|
||||||
usePreferencesQuery,
|
usePreferencesQuery,
|
||||||
usePreferencesSetContentLabelMutation,
|
usePreferencesSetContentLabelMutation,
|
||||||
} from '#/state/queries/preferences'
|
} from '#/state/queries/preferences'
|
||||||
import {useLabelBehaviorDescription} from '#/lib/moderation/useLabelBehaviorDescription'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {getLabelStrings} from '#/lib/moderation/useLabelInfo'
|
|
||||||
|
|
||||||
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import {InlineLink} from '#/components/Link'
|
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo'
|
|
||||||
import * as ToggleButton from '#/components/forms/ToggleButton'
|
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||||
|
import {InlineLink} from '#/components/Link'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo'
|
||||||
|
|
||||||
export function Outer({children}: React.PropsWithChildren<{}>) {
|
export function Outer({children}: React.PropsWithChildren<{}>) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import VersionNumber from 'react-native-version-number'
|
import VersionNumber from 'react-native-version-number'
|
||||||
import * as Updates from 'expo-updates'
|
|
||||||
export const updateChannel = Updates.channel
|
|
||||||
|
|
||||||
export const appVersion = `${VersionNumber.appVersion} (${VersionNumber.buildVersion})`
|
export const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
|
||||||
|
export const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
|
||||||
|
|
||||||
|
const UPDATES_CHANNEL = IS_TESTFLIGHT ? 'testflight' : 'production'
|
||||||
|
export const appVersion = `${VersionNumber.appVersion} (${
|
||||||
|
VersionNumber.buildVersion
|
||||||
|
}, ${IS_DEV ? 'development' : UPDATES_CHANNEL})`
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {Alert, AppState, AppStateStatus} from 'react-native'
|
||||||
|
import app from 'react-native-version-number'
|
||||||
|
import {
|
||||||
|
checkForUpdateAsync,
|
||||||
|
fetchUpdateAsync,
|
||||||
|
isEnabled,
|
||||||
|
reloadAsync,
|
||||||
|
setExtraParamAsync,
|
||||||
|
useUpdates,
|
||||||
|
} from 'expo-updates'
|
||||||
|
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {IS_TESTFLIGHT} from 'lib/app-info'
|
||||||
|
import {isIOS} from 'platform/detection'
|
||||||
|
|
||||||
|
const MINIMUM_MINIMIZE_TIME = 15 * 60e3
|
||||||
|
|
||||||
|
async function setExtraParams() {
|
||||||
|
await setExtraParamAsync(
|
||||||
|
isIOS ? 'ios-build-number' : 'android-build-number',
|
||||||
|
// Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is.
|
||||||
|
// This just ensures it gets passed as a string
|
||||||
|
`${app.buildVersion}`,
|
||||||
|
)
|
||||||
|
await setExtraParamAsync(
|
||||||
|
'channel',
|
||||||
|
IS_TESTFLIGHT ? 'testflight' : 'production',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOTAUpdates() {
|
||||||
|
const appState = React.useRef<AppStateStatus>('active')
|
||||||
|
const lastMinimize = React.useRef(0)
|
||||||
|
const ranInitialCheck = React.useRef(false)
|
||||||
|
const timeout = React.useRef<NodeJS.Timeout>()
|
||||||
|
const {isUpdatePending} = useUpdates()
|
||||||
|
|
||||||
|
const setCheckTimeout = React.useCallback(() => {
|
||||||
|
timeout.current = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await setExtraParams()
|
||||||
|
|
||||||
|
logger.debug('Checking for update...')
|
||||||
|
const res = await checkForUpdateAsync()
|
||||||
|
|
||||||
|
if (res.isAvailable) {
|
||||||
|
logger.debug('Attempting to fetch update...')
|
||||||
|
await fetchUpdateAsync()
|
||||||
|
} else {
|
||||||
|
logger.debug('No update available.')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('OTA Update Error', {error: `${e}`})
|
||||||
|
}
|
||||||
|
}, 10e3)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onIsTestFlight = React.useCallback(() => {
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await setExtraParams()
|
||||||
|
|
||||||
|
const res = await checkForUpdateAsync()
|
||||||
|
if (res.isAvailable) {
|
||||||
|
await fetchUpdateAsync()
|
||||||
|
|
||||||
|
Alert.alert(
|
||||||
|
'Update Available',
|
||||||
|
'A new version of the app is available. Relaunch now?',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'No',
|
||||||
|
style: 'cancel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Relaunch',
|
||||||
|
style: 'default',
|
||||||
|
onPress: async () => {
|
||||||
|
await reloadAsync()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
// No need to handle
|
||||||
|
}
|
||||||
|
}, 3e3)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// For Testflight users, we can prompt the user to update immediately whenever there's an available update. This
|
||||||
|
// is suspect however with the Apple App Store guidelines, so we don't want to prompt production users to update
|
||||||
|
// immediately.
|
||||||
|
if (IS_TESTFLIGHT) {
|
||||||
|
onIsTestFlight()
|
||||||
|
return
|
||||||
|
} else if (!isEnabled || __DEV__ || ranInitialCheck.current) {
|
||||||
|
// Development client shouldn't check for updates at all, so we skip that here.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setCheckTimeout()
|
||||||
|
ranInitialCheck.current = true
|
||||||
|
}, [onIsTestFlight, setCheckTimeout])
|
||||||
|
|
||||||
|
// After the app has been minimized for 30 minutes, we want to either A. install an update if one has become available
|
||||||
|
// or B check for an update again.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isEnabled) return
|
||||||
|
|
||||||
|
const subscription = AppState.addEventListener(
|
||||||
|
'change',
|
||||||
|
async nextAppState => {
|
||||||
|
if (
|
||||||
|
appState.current.match(/inactive|background/) &&
|
||||||
|
nextAppState === 'active'
|
||||||
|
) {
|
||||||
|
// If it's been 15 minutes since the last "minimize", we should feel comfortable updating the client since
|
||||||
|
// chances are that there isn't anything important going on in the current session.
|
||||||
|
if (lastMinimize.current <= Date.now() - MINIMUM_MINIMIZE_TIME) {
|
||||||
|
if (isUpdatePending) {
|
||||||
|
await reloadAsync()
|
||||||
|
} else {
|
||||||
|
setCheckTimeout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lastMinimize.current = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
appState.current = nextAppState
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeout.current)
|
||||||
|
subscription.remove()
|
||||||
|
}
|
||||||
|
}, [isUpdatePending, setCheckTimeout])
|
||||||
|
}
|
|
@ -1,51 +1,49 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
|
||||||
import {ComAtprotoLabelDefs} from '@atproto/api'
|
|
||||||
import {Trans, msg} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {LABELS} from '@atproto/api'
|
|
||||||
import {useSafeAreaFrame} from 'react-native-safe-area-context'
|
import {useSafeAreaFrame} from 'react-native-safe-area-context'
|
||||||
|
import {ComAtprotoLabelDefs} from '@atproto/api'
|
||||||
|
import {LABELS} from '@atproto/api'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
|
|
||||||
import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types'
|
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||||
import {CenteredView} from '#/view/com/util/Views'
|
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
import {logger} from '#/logger'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {
|
||||||
import {useSetMinimalShellMode} from '#/state/shell'
|
useMyLabelersQuery,
|
||||||
import {useSession} from '#/state/session'
|
usePreferencesQuery,
|
||||||
|
UsePreferencesQueryResponse,
|
||||||
|
usePreferencesSetAdultContentMutation,
|
||||||
|
} from '#/state/queries/preferences'
|
||||||
import {
|
import {
|
||||||
useProfileQuery,
|
useProfileQuery,
|
||||||
useProfileUpdateMutation,
|
useProfileUpdateMutation,
|
||||||
} from '#/state/queries/profile'
|
} from '#/state/queries/profile'
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
|
import {useSetMinimalShellMode} from '#/state/shell'
|
||||||
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||||
|
import {CenteredView} from '#/view/com/util/Views'
|
||||||
import {ScrollView} from '#/view/com/util/Views'
|
import {ScrollView} from '#/view/com/util/Views'
|
||||||
|
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp} from '#/alf'
|
||||||
import {
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
UsePreferencesQueryResponse,
|
import * as Dialog from '#/components/Dialog'
|
||||||
useMyLabelersQuery,
|
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
|
||||||
usePreferencesQuery,
|
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||||
usePreferencesSetAdultContentMutation,
|
|
||||||
} from '#/state/queries/preferences'
|
|
||||||
|
|
||||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {useTheme, atoms as a, useBreakpoints, ViewStyleProp} from '#/alf'
|
|
||||||
import {Divider} from '#/components/Divider'
|
import {Divider} from '#/components/Divider'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||||
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
|
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
|
||||||
|
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||||
|
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
|
||||||
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
|
||||||
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
|
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
|
||||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
|
||||||
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
|
||||||
import {InlineLink, Link} from '#/components/Link'
|
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
|
||||||
import {Loader} from '#/components/Loader'
|
|
||||||
import * as LabelingService from '#/components/LabelingServiceCard'
|
import * as LabelingService from '#/components/LabelingServiceCard'
|
||||||
|
import {InlineLink, Link} from '#/components/Link'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
import {GlobalLabelPreference} from '#/components/moderation/LabelPreference'
|
import {GlobalLabelPreference} from '#/components/moderation/LabelPreference'
|
||||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
import {Text} from '#/components/Typography'
|
||||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
|
||||||
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
|
|
||||||
import * as Dialog from '#/components/Dialog'
|
|
||||||
|
|
||||||
function ErrorState({error}: {error: string}) {
|
function ErrorState({error}: {error: string}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
|
@ -1,30 +1,29 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
import {useSafeAreaFrame} from 'react-native-safe-area-context'
|
||||||
import {
|
import {
|
||||||
AppBskyLabelerDefs,
|
AppBskyLabelerDefs,
|
||||||
ModerationOpts,
|
|
||||||
interpretLabelValueDefinitions,
|
|
||||||
InterpretedLabelValueDefinition,
|
InterpretedLabelValueDefinition,
|
||||||
|
interpretLabelValueDefinitions,
|
||||||
|
ModerationOpts,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {Trans, msg} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useSafeAreaFrame} from 'react-native-safe-area-context'
|
|
||||||
|
|
||||||
import {useScrollHandlers} from '#/lib/ScrollContext'
|
|
||||||
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
||||||
import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
|
import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
|
||||||
import {ListRef} from '#/view/com/util/List'
|
import {useScrollHandlers} from '#/lib/ScrollContext'
|
||||||
import {SectionRef} from './types'
|
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
|
import {ListRef} from '#/view/com/util/List'
|
||||||
import {useTheme, atoms as a} from '#/alf'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import {Loader} from '#/components/Loader'
|
|
||||||
import {Divider} from '#/components/Divider'
|
|
||||||
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
import {CenteredView, ScrollView} from '#/view/com/util/Views'
|
||||||
import {ErrorState} from '../ErrorState'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {LabelerLabelPreference} from '#/components/moderation/LabelPreference'
|
import {Divider} from '#/components/Divider'
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {LabelerLabelPreference} from '#/components/moderation/LabelPreference'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {ErrorState} from '../ErrorState'
|
||||||
|
import {SectionRef} from './types'
|
||||||
|
|
||||||
interface LabelsSectionProps {
|
interface LabelsSectionProps {
|
||||||
isLabelerLoading: boolean
|
isLabelerLoading: boolean
|
||||||
|
|
|
@ -3,72 +3,72 @@ import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Linking,
|
Linking,
|
||||||
Platform,
|
Platform,
|
||||||
StyleSheet,
|
|
||||||
Pressable,
|
Pressable,
|
||||||
|
StyleSheet,
|
||||||
TextStyle,
|
TextStyle,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
ViewStyle,
|
ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {useFocusEffect, useNavigation} from '@react-navigation/native'
|
|
||||||
import {
|
import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import * as AppInfo from 'lib/app-info'
|
import {useLingui} from '@lingui/react'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {useCustomPalette} from 'lib/hooks/useCustomPalette'
|
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
|
||||||
import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
|
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
|
||||||
import {HandIcon, HashtagIcon} from 'lib/icons'
|
|
||||||
import Clipboard from '@react-native-clipboard/clipboard'
|
import Clipboard from '@react-native-clipboard/clipboard'
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
import {useFocusEffect, useNavigation} from '@react-navigation/native'
|
||||||
import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {isNative} from '#/platform/detection'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {
|
import {clearLegacyStorage} from '#/state/persisted/legacy'
|
||||||
useSetMinimalShellMode,
|
// TODO import {useInviteCodesQuery} from '#/state/queries/invites'
|
||||||
useThemePrefs,
|
import {clear as clearStorage} from '#/state/persisted/store'
|
||||||
useSetThemePrefs,
|
|
||||||
useOnboardingDispatch,
|
|
||||||
} from '#/state/shell'
|
|
||||||
import {
|
import {
|
||||||
useRequireAltTextEnabled,
|
useRequireAltTextEnabled,
|
||||||
useSetRequireAltTextEnabled,
|
useSetRequireAltTextEnabled,
|
||||||
} from '#/state/preferences'
|
} from '#/state/preferences'
|
||||||
import {useSession, useSessionApi, SessionAccount} from '#/state/session'
|
|
||||||
import {useProfileQuery} from '#/state/queries/profile'
|
|
||||||
import {useClearPreferencesMutation} from '#/state/queries/preferences'
|
|
||||||
// TODO import {useInviteCodesQuery} from '#/state/queries/invites'
|
|
||||||
import {clear as clearStorage} from '#/state/persisted/store'
|
|
||||||
import {clearLegacyStorage} from '#/state/persisted/legacy'
|
|
||||||
import {STATUS_PAGE_URL} from 'lib/constants'
|
|
||||||
import {Trans, msg} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
|
||||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
|
||||||
import {useCloseAllActiveElements} from '#/state/util'
|
|
||||||
import {
|
import {
|
||||||
useInAppBrowser,
|
useInAppBrowser,
|
||||||
useSetInAppBrowser,
|
useSetInAppBrowser,
|
||||||
} from '#/state/preferences/in-app-browser'
|
} from '#/state/preferences/in-app-browser'
|
||||||
import {isNative} from '#/platform/detection'
|
import {useClearPreferencesMutation} from '#/state/queries/preferences'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
|
||||||
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {s, colors} from 'lib/styles'
|
import {SessionAccount, useSession, useSessionApi} from '#/state/session'
|
||||||
import {ScrollView} from 'view/com/util/Views'
|
import {
|
||||||
|
useOnboardingDispatch,
|
||||||
|
useSetMinimalShellMode,
|
||||||
|
useSetThemePrefs,
|
||||||
|
useThemePrefs,
|
||||||
|
} from '#/state/shell'
|
||||||
|
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
|
import {useCloseAllActiveElements} from '#/state/util'
|
||||||
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
import * as AppInfo from 'lib/app-info'
|
||||||
|
import {STATUS_PAGE_URL} from 'lib/constants'
|
||||||
|
import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
|
||||||
|
import {useCustomPalette} from 'lib/hooks/useCustomPalette'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
|
import {HandIcon, HashtagIcon} from 'lib/icons'
|
||||||
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
|
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
|
||||||
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
|
import {colors, s} from 'lib/styles'
|
||||||
|
import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn'
|
||||||
|
import {SelectableBtn} from 'view/com/util/forms/SelectableBtn'
|
||||||
|
import {ToggleButton} from 'view/com/util/forms/ToggleButton'
|
||||||
import {Link, TextLink} from 'view/com/util/Link'
|
import {Link, TextLink} from 'view/com/util/Link'
|
||||||
|
import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
|
||||||
import {Text} from 'view/com/util/text/Text'
|
import {Text} from 'view/com/util/text/Text'
|
||||||
import * as Toast from 'view/com/util/Toast'
|
import * as Toast from 'view/com/util/Toast'
|
||||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||||
import {ToggleButton} from 'view/com/util/forms/ToggleButton'
|
import {ScrollView} from 'view/com/util/Views'
|
||||||
import {SelectableBtn} from 'view/com/util/forms/SelectableBtn'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn'
|
|
||||||
import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
|
|
||||||
import {ExportCarDialog} from './ExportCarDialog'
|
|
||||||
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
|
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
|
||||||
|
import {ExportCarDialog} from './ExportCarDialog'
|
||||||
|
|
||||||
function SettingsAccountCard({account}: {account: SessionAccount}) {
|
function SettingsAccountCard({account}: {account: SessionAccount}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
@ -890,9 +890,7 @@ export function SettingsScreen({}: Props) {
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
onPress={onPressBuildInfo}>
|
onPress={onPressBuildInfo}>
|
||||||
<Text type="sm" style={[styles.buildInfo, pal.textLight]}>
|
<Text type="sm" style={[styles.buildInfo, pal.textLight]}>
|
||||||
<Trans>
|
<Trans>Version {AppInfo.appVersion}</Trans>
|
||||||
Build version {AppInfo.appVersion} {AppInfo.updateChannel}
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text type="sm" style={[pal.textLight]}>
|
<Text type="sm" style={[pal.textLight]}>
|
||||||
|
|
Loading…
Reference in New Issue