Merge branch 'main' into hailey/app-clip-clop

# Conflicts:
#	package.json
#	yarn.lock
This commit is contained in:
Hailey
2024-06-03 08:48:27 -07:00
258 changed files with 5223 additions and 9947 deletions
@@ -119,3 +119,15 @@ jobs:
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
if: ${{ inputs.profile == 'testflight' }}
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
if: ${{ inputs.profile == 'testflight' }}
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
+16
View File
@@ -47,6 +47,10 @@ jobs:
- name: ⚙️ Install dependencies
run: yarn install
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.3'
- name: ☕️ Setup Cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
with:
@@ -76,3 +80,15 @@ jobs:
- name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path build.ipa
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
if: ${{ inputs.profile == 'testflight' }}
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
if: ${{ inputs.profile == 'testflight' }}
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
+53 -63
View File
@@ -26,7 +26,7 @@ jobs:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-deploy
cancel-in-progress: true
outputs:
fingerprint-is-different: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different }}
changes-detected: ${{ steps.fingerprint.outputs.includes-changes }}
steps:
- name: Check for EXPO_TOKEN
@@ -49,69 +49,22 @@ jobs:
with:
fetch-depth: 0
- name: ⬇️ Get last successful deployment commit from the cache
id: get-base-commit
uses: actions/cache@v4
with:
path: last-successful-commit-hash.txt
key: last-successful-deployment-commit-${{ github.ref_name }}-${{ github.sha }}
restore-keys: |
last-successful-deployment-commit-${{ github.ref_name }}-
- name: Add the last successful deployment commit to the output
id: last-successful-commit
run: echo base-commit=$(cat last-successful-commit-hash.txt) >> "$GITHUB_OUTPUT"
- name: ⬇️ Fetch commits from base branch
if: ${{ github.ref != 'refs/heads/main' }}
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 ${{ inputs.channel == 'production' }}; then
echo base-commit=$(git show-ref -s ${{ inputs.runtimeVersion }}) >> "$GITHUB_OUTPUT"
else
echo base-commit=${{ steps.last-successful-commit.base-commit }} >> "$GITHUB_OUTPUT"
fi
- name: ✓ Make sure we found a base commit
run: |
if [ -z "${{ steps.base-commit.outputs.base-commit }}" && ${{ inputs.channel == 'production' }} ]; then
echo "Could not find a base commit for this release. Exiting."
exit 1
fi
- name: 🔧 Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: yarn
- name: ⚙️ Install Dependencies
run: yarn install
# Run the fingerprint
- name: 📷 Check fingerprint
- name: 📷 Check fingerprint and install dependencies
id: fingerprint
uses: expo/expo-github-action/fingerprint@main
uses: bluesky-social/github-actions/fingerprint-native@main
with:
previous-git-commit: ${{ steps.base-commit.outputs.base-commit }}
- name: 👀 Debug fingerprint
id: fingerprint-debug
run: |
echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}"
echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}"
fingerprintDiff='$(echo "${{ steps.fingerprint.outputs.fingerprint-diff }}")'
if [[ $fingerprintDiff =~ "bareRncliAutolinking" || $fingerprintDiff =~ "expoAutolinkingAndroid" || $fingerprintDiff =~ "expoAutolinkingIos" ]]; then
echo fingerprint-is-different="true" >> "$GITHUB_OUTPUT"
else
echo fingerprint-is-different="false" >> "$GITHUB_OUTPUT"
fi
profile: ${{ inputs.channel || 'testflight' }}
previous-commit-tag: ${{ inputs.runtimeVersion }}
- name: Lint check
run: yarn lint
@@ -127,22 +80,22 @@ jobs:
- name: 🔨 Setup EAS
uses: expo/expo-github-action@v8
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
if: ${{ !steps.fingerprint.outputs.includes-changes }}
with:
expo-version: latest
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup Expo
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
if: ${{ !steps.fingerprint.outputs.includes-changes }}
run: yarn global add eas-cli-local-build-plugin
- name: 🪛 Setup jq
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
if: ${{ !steps.fingerprint.outputs.includes-changes }}
uses: dcarbone/install-jq-action@v2
- name: ✏️ Write environment variables
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
if: ${{ !steps.fingerprint.outputs.includes-changes }}
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
@@ -151,19 +104,28 @@ jobs:
echo "$json" > google-services.json
- name: 🏗️ Create Bundle
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
if: ${{ !steps.fingerprint.outputs.includes-changes }}
run: EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export
- name: 📦 Package Bundle and 🚀 Deploy
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
if: ${{ !steps.fingerprint.outputs.includes-changes }}
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' }}
- name: Save successful deployment commit hash
run: echo ${{ steps.fingerprint.outputs.current-git-commit }} > last-successful-commit-hash.txt
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
if: ${{ !steps.fingerprint.outputs.includes-changes }}
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
if: ${{ !steps.fingerprint.outputs.includes-changes }}
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
# GitHub actions are horrible so let's just copy paste this in
buildIfNecessaryIOS:
@@ -171,11 +133,11 @@ jobs:
runs-on: macos-14
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
cancel-in-progress: false
cancel-in-progress: true
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-is-different == 'true' }}
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected }}
steps:
- name: Check for EXPO_TOKEN
run: >
@@ -208,6 +170,10 @@ jobs:
- name: ⚙️ Install dependencies
run: yarn install
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.3'
- name: ☕️ Setup Cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
with:
@@ -238,6 +204,18 @@ jobs:
- name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path build.ipa
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
if: ${{ inputs.channel == 'testflight' }}
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
if: ${{ inputs.channel == 'testflight' }}
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
buildIfNecessaryAndroid:
name: Build and Submit Android
runs-on: ubuntu-latest
@@ -247,7 +225,7 @@ jobs:
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-is-different == 'true' }}
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.changes-detected }}
steps:
- name: Check for EXPO_TOKEN
@@ -325,3 +303,15 @@ jobs:
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
if: ${{ inputs.channel != 'testflight' && inputs.channel != 'production' }}
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
if: ${{ inputs.channel != 'testflight' && inputs.channel != 'production' }}
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
+11 -24
View File
@@ -1,7 +1,7 @@
# Credit for fingerprint action https://github.com/expo/expo
# https://github.com/expo/expo/blob/main/.github/workflows/pr-labeler.yml
---
name: PR labeler
name: PR Tests
on:
push:
@@ -21,7 +21,7 @@ permissions:
jobs:
webpack-analyzer:
runs-on: ubuntu-22.04
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}}
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
@@ -94,10 +94,9 @@ jobs:
| ${{ steps.get-diff.outputs.base_file_string }} | ${{ steps.get-diff.outputs.pr_file_string }} | ${{ steps.get-diff.outputs.diff_file_string }} (${{ steps.get-diff.outputs.percent }}%) |
---
test-suite-fingerprint:
fingerprint-native:
runs-on: ubuntu-22.04
if: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' }}
concurrency: fingerprint-${{ github.event_name != 'pull_request' && 'main' || github.run_id }}
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'}}
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
@@ -114,35 +113,23 @@ jobs:
node-version-file: .nvmrc
cache: yarn
- name: ⚙️ Install Dependencies
run: yarn install
- name: Get the base commit
id: base-commit
run: echo base-commit=$(git log -n 1 main --pretty=format:'%H') >> "$GITHUB_OUTPUT"
- name: 📷 Check fingerprint
- name: 📷 Check fingerprint and install dependencies
id: fingerprint
uses: expo/expo-github-action/fingerprint@main
uses: bluesky-social/github-actions/fingerprint-native@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 == '' }}"
profile: pull-request
- name: 💬 Drop a comment
uses: marocchino/sticky-pull-request-comment@v2
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' }}
if: ${{ steps.fingerprint.outputs.includes-changes }}
with:
header: fingerprint-diff
message: |
The Pull Request introduced fingerprint changes against the base commit: ${{ steps.fingerprint.outputs.previous-git-commit }}
The Pull Request introduced fingerprint changes against the base commit:
<details><summary>Fingerprint diff</summary>
```json
${{ steps.fingerprint.outputs.fingerprint-diff }}
${{ steps.fingerprint.outputs.diff }}
```
</details>
@@ -152,7 +139,7 @@ jobs:
- name: 💬 Delete comment
uses: marocchino/sticky-pull-request-comment@v2
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff == '[]' }}
if: ${{ !steps.fingerprint.outputs.includes-changes }}
with:
header: fingerprint-diff
delete: true
+2 -1
View File
@@ -142,7 +142,8 @@ describe('makeRecordUri', () => {
})
})
describe('ago', () => {
// FIXME: Reenable after fixing non-deterministic test.
describe.skip('ago', () => {
const oneYearDate = new Date(
new Date().setMonth(new Date().getMonth() - 11),
).setDate(new Date().getDate() - 28)
+2 -2
View File
@@ -175,7 +175,6 @@ module.exports = function (config) {
checkAutomatically: 'NEVER',
channel: UPDATES_CHANNEL,
},
assetBundlePatterns: ['**/*'],
plugins: [
'expo-localization',
Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
@@ -183,7 +182,7 @@ module.exports = function (config) {
'expo-build-properties',
{
ios: {
deploymentTarget: '13.4',
deploymentTarget: '14.0',
newArchEnabled: false,
},
android: {
@@ -212,6 +211,7 @@ module.exports = function (config) {
],
'./plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js',
'./plugins/withAndroidManifestLaunchModePlugin.js',
'./plugins/withAndroidStylesWindowBackgroundPlugin.js',
'./plugins/withAndroidStylesAccentColorPlugin.js',
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M3.004 4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2v5a1 1 0 0 0 .43.822c.57.395 1.176.031 1.685-.255.428-.24.998-.614 1.569-1.15 1.154-1.082 2.316-2.832 2.316-5.417V5a1 1 0 0 0-1-1h-7ZM14.004 4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2v5a1 1 0 0 0 .43.822c.57.395 1.176.031 1.685-.255.428-.24.998-.614 1.569-1.15 1.154-1.082 2.316-2.832 2.316-5.417V5a1 1 0 0 0-1-1h-7Z"/></svg>

After

Width:  |  Height:  |  Size: 450 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M2.004 5a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v8c0 2.585-1.162 4.335-2.316 5.417-.571.536-1.14.91-1.569 1.15a7.01 7.01 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001L6.004 19l.351.936A1 1 0 0 1 5.004 19v-5h-2a1 1 0 0 1-1-1V5Zm5 12.234c.104-.085.21-.177.316-.276.846-.793 1.684-2.043 1.684-3.958V6h-5v6h2a1 1 0 0 1 1 1v4.234Zm6-12.234a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v8c0 2.585-1.162 4.335-2.316 5.417-.571.536-1.14.91-1.569 1.15a7.018 7.018 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001-.352-.936.351.936A1 1 0 0 1 16.004 19v-5h-2a1 1 0 0 1-1-1V5Zm5 12.234V13a1 1 0 0 0-1-1h-2V6h5v7c0 1.915-.838 3.165-1.684 3.958-.106.1-.212.191-.316.276Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 775 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M2.003 5.999a2 2 0 0 1 2-1.999h5c1.104 0 2 .893 2 1.999V13c0 2.585-1.16 4.335-2.315 5.417-.571.536-1.14.91-1.569 1.15a7.01 7.01 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001L6.004 19l.351.936a1 1 0 0 1-1.351-.935L5 14H4a2 2 0 0 1-2-2.001l.002-6Zm5 11.236L7 12.999A1 1 0 0 0 6 12H4l.003-6h5v7c0 1.915-.837 3.165-1.683 3.958-.106.1-.213.192-.317.277Zm6-11.235a2 2 0 0 1 2-2h5c1.104 0 2 .893 2 1.999V13c0 2.585-1.16 4.335-2.315 5.417-.571.536-1.14.91-1.569 1.15a7.018 7.018 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001-.352-.936.351.936A1 1 0 0 1 16.004 19v-5h-1a2 2 0 0 1-2-2V6Zm7 0h-5v6h2a1 1 0 0 1 1 1v4.234c.105-.085.211-.177.317-.276.846-.793 1.684-2.043 1.684-3.958V6Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 821 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M8.004 5a1 1 0 0 0-.43-.822c-.57-.395-1.176-.031-1.685.255-.428.24-.998.614-1.569 1.15C3.166 6.665 2.004 8.415 2.004 11v8a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1v-8a1 1 0 0 0-1-1h-2V5ZM19.004 5a1 1 0 0 0-.43-.822c-.57-.395-1.176-.031-1.685.255-.428.24-.998.614-1.569 1.15-1.154 1.082-2.316 2.832-2.316 5.417v8a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1v-8a1 1 0 0 0-1-1h-2V5Z"/></svg>

After

Width:  |  Height:  |  Size: 455 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M7.574 4.178a1 1 0 0 1 .43.822v5h2a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-8c0-2.585 1.162-4.335 2.316-5.417a8.163 8.163 0 0 1 1.569-1.15 7.029 7.029 0 0 1 .738-.36l.016-.005.005-.003h.003v-.001c.001 0 .002 0 .353.936l-.351-.936a1 1 0 0 1 .92.114Zm-1.57 2.588a5.99 5.99 0 0 0-.316.276C4.842 7.835 4.004 9.085 4.004 11v7h5v-6h-2a1 1 0 0 1-1-1V6.766Zm12.57-2.588a1 1 0 0 1 .43.822v5h2a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-8c0-2.585 1.162-4.335 2.316-5.417a8.166 8.166 0 0 1 1.569-1.15 7.038 7.038 0 0 1 .738-.36l.016-.005.005-.003h.003v-.001c.001 0 .002 0 .353.936l-.351-.936a1 1 0 0 1 .92.114Zm-1.57 2.588c-.105.085-.21.177-.316.276-.846.793-1.684 2.043-1.684 3.958v7h5v-6h-2a1 1 0 0 1-1-1V6.766Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 857 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM12 12c-4.758 0-8.083 3.521-8.496 7.906A1 1 0 0 0 4.5 21H15a3 3 0 1 1 0-6c0-.824.332-1.571.87-2.113C14.739 12.32 13.435 12 12 12Zm6 2a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 412 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.678 19c.71-2.909 3.092-5 6.322-5 .621 0 1.206.077 1.748.218a1 1 0 1 0 .504-1.936A8.931 8.931 0 0 0 12 12c-4.758 0-8.083 3.521-8.496 7.906A1 1 0 0 0 4.5 21H11a1 1 0 1 0 0-2H5.678ZM18 14a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 508 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M16.293 2.293a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-3 3a1 1 0 0 1-1.414-1.414L17.586 7H5v4a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1h13.586l-1.293-1.293a1 1 0 0 1 0-1.414ZM21 13v5a1 1 0 0 1-1 1H6.414l1.293 1.293a1 1 0 1 1-1.414 1.414l-3-3a1 1 0 0 1 0-1.414l3-3a1 1 0 0 1 1.414 1.414L6.414 17H19v-4a1 1 0 1 1 2 0Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 446 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M16.793 2.293a1 1 0 0 1 1.414 0L20.5 4.586a2 2 0 0 1 0 2.828l-2.293 2.293a1 1 0 0 1-1.414-1.414L18.086 7H7a2 2 0 0 0-2 2v2a1 1 0 1 1-2 0V9a4 4 0 0 1 4-4h11.086l-1.293-1.293a1 1 0 0 1 0-1.414ZM20 12a1 1 0 0 1 1 1v2a4 4 0 0 1-4 4H5.914l1.293 1.293a1 1 0 1 1-1.414 1.414L3.5 19.414a2 2 0 0 1 0-2.828l2.293-2.293a1 1 0 0 1 1.414 1.414L5.914 17H17a2 2 0 0 0 2-2v-2a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 517 B

-3
View File
@@ -8,9 +8,6 @@ module.exports = function (api) {
{
lazyImports: true,
native: {
// We should be able to remove this after upgrading Expo
// to a version that includes https://github.com/expo/expo/pull/24672.
unstable_transformProfile: 'hermes-stable',
// Disable ESM -> CJS compilation because Metro takes care of it.
// However, we need it in Jest tests since those run without Metro.
disableImportExportTransform: !isTestEnv,
+4 -1
View File
@@ -4,7 +4,10 @@
- Set up your environment [using the expo instructions](https://docs.expo.dev/guides/local-app-development/).
- make sure that the JAVA_HOME points to the zulu-17 directory in your `.zshrc` or `.bashrc` file: `export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home`. DO NOT use another JDK or you will encounter build errors.
- If you're running macOS, make sure you are running the correct versions of Ruby and Cocoapods:
- If you're running macOS, make sure you are running the correct versions of Ruby and Cocoapods:-
- If you are using Apple Silicon and this is the first time you are building for RN 0.74+, you may need to run:
- `arch -arm64 brew install llvm`
- `sudo gem install ffi`
- Check if you've installed Cocoapods through `homebrew`. If you have, remove it:
- `brew info cocoapods`
- If output says `Installed`:
+7 -2
View File
@@ -1,10 +1,10 @@
/* global jest */
import {configure} from '@testing-library/react-native'
import 'react-native-gesture-handler/jestSetup'
// IMPORTANT: this is what's used in the native runtime
import 'react-native-url-polyfill/auto'
import {configure} from '@testing-library/react-native'
configure({asyncUtilTimeout: 20000})
jest.mock('@react-native-async-storage/async-storage', () =>
@@ -90,3 +90,8 @@ jest.mock('sentry-expo', () => ({
}))
jest.mock('crypto', () => ({}))
jest.mock('expo-application', () => ({
nativeApplicationVersion: '1.0.0',
nativeBuildVersion: '1',
}))
@@ -10,8 +10,8 @@ Pod::Spec.new do |s|
s.static_framework = true
s.dependency 'ExpoModulesCore'
s.dependency 'SDWebImage', '~> 5.17.0'
s.dependency 'SDWebImageWebPCoder', '~> 0.13.0'
s.dependency 'SDWebImage', '~> 5.19.1'
s.dependency 'SDWebImageWebPCoder', '~> 0.14.6'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
@@ -0,0 +1,6 @@
{
"platforms": ["ios"],
"ios": {
"modules": ["ExpoBlueskyTranslateModule"]
}
}
+6
View File
@@ -0,0 +1,6 @@
export {
isAvailable,
isLanguageSupported,
NativeTranslationModule,
NativeTranslationView,
} from './src/ExpoBlueskyTranslateView'
@@ -0,0 +1,20 @@
import ExpoModulesCore
import SwiftUI
// Thanks to Andrew Levy for this code snippet
// https://github.com/andrew-levy/swiftui-react-native/blob/d3fbb2abf07601ff0d4b83055e7717bb980910d6/ios/Common/ExpoView%2BUIHostingController.swift
extension ExpoView {
func setupHostingController(_ hostingController: UIHostingController<some View>) {
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
hostingController.view.backgroundColor = .clear
addSubview(hostingController.view)
NSLayoutConstraint.activate([
hostingController.view.topAnchor.constraint(equalTo: self.topAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: self.bottomAnchor),
hostingController.view.leftAnchor.constraint(equalTo: self.leftAnchor),
hostingController.view.rightAnchor.constraint(equalTo: self.rightAnchor),
])
}
}
@@ -0,0 +1,21 @@
Pod::Spec.new do |s|
s.name = 'ExpoBlueskyTranslate'
s.version = '1.0.0'
s.summary = 'Uses SwiftUI translation to translate text.'
s.description = 'Uses SwiftUI translation to translate text.'
s.author = ''
s.homepage = 'https://docs.expo.dev/modules/'
s.platforms = { :ios => '13.4' }
s.source = { git: '' }
s.static_framework = true
s.dependency 'ExpoModulesCore'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
end
@@ -0,0 +1,18 @@
import ExpoModulesCore
import Foundation
import SwiftUI
public class ExpoBlueskyTranslateModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoBlueskyTranslate")
AsyncFunction("presentAsync") { (text: String) in
DispatchQueue.main.async { [weak state = TranslateViewState.shared] in
state?.isPresented = true
state?.text = text
}
}
View(ExpoBlueskyTranslateView.self) {}
}
}
@@ -0,0 +1,22 @@
import ExpoModulesCore
import Foundation
import SwiftUI
class TranslateViewState: ObservableObject {
static var shared = TranslateViewState()
@Published var isPresented = false
@Published var text = ""
}
class ExpoBlueskyTranslateView: ExpoView {
required init(appContext: AppContext? = nil) {
if #available(iOS 14.0, *) {
let hostingController = UIHostingController(rootView: TranslateView())
super.init(appContext: appContext)
setupHostingController(hostingController)
} else {
super.init(appContext: appContext)
}
}
}
@@ -0,0 +1,31 @@
import SwiftUI
// conditionally import the Translation module
#if canImport(Translation)
import Translation
#endif
struct TranslateView: View {
@ObservedObject var state = TranslateViewState.shared
var body: some View {
if #available(iOS 17.4, *) {
VStack {
UIViewRepresentableWrapper(view: UIView(frame: .zero))
}
.translationPresentation(
isPresented: $state.isPresented,
text: state.text
)
}
}
}
struct UIViewRepresentableWrapper: UIViewRepresentable {
let view: UIView
func makeUIView(context: Context) -> UIView {
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
@@ -0,0 +1,3 @@
export type ExpoBlueskyTranslateModule = {
presentAsync: (text: string) => Promise<void>
}
@@ -0,0 +1,51 @@
import React from 'react'
import {Platform} from 'react-native'
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
import {ExpoBlueskyTranslateModule} from './ExpoBlueskyTranslate.types'
export const NativeTranslationModule =
requireNativeModule<ExpoBlueskyTranslateModule>('ExpoBlueskyTranslate')
const NativeView: React.ComponentType = requireNativeViewManager(
'ExpoBlueskyTranslate',
)
export function NativeTranslationView() {
return <NativeView />
}
// can be something like "17.5.1", so just take the first two parts
const version = String(Platform.Version).split('.').slice(0, 2).join('.')
export const isAvailable = Number(version) >= 17.4
// https://en.wikipedia.org/wiki/Translate_(Apple)#Languages
const SUPPORTED_LANGUAGES = [
'ar',
'zh',
'zh',
'nl',
'en',
'en',
'fr',
'de',
'id',
'it',
'ja',
'ko',
'pl',
'pt',
'ru',
'es',
'th',
'tr',
'uk',
'vi',
]
export function isLanguageSupported(lang?: string) {
// If the language is not provided, we assume it is supported
if (!lang) return true
return SUPPORTED_LANGUAGES.includes(lang)
}
@@ -0,0 +1,13 @@
export const NativeTranslationModule = {
presentAsync: async (_: string) => {},
}
export function NativeTranslationView() {
return null
}
export const isAvailable = false
export function isLanguageSupported(_lang?: string) {
return false
}
@@ -1,5 +1,7 @@
import React from 'react'
import {ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types'
export function ExpoScrollForwarderView({
children,
}: React.PropsWithChildren<ExpoScrollForwarderViewProps>) {
+53 -51
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.84.0",
"version": "1.85.0",
"private": true,
"engines": {
"node": ">=18"
@@ -49,7 +49,7 @@
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
},
"dependencies": {
"@atproto/api": "^0.12.13",
"@atproto/api": "^0.12.14",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
@@ -63,19 +63,19 @@
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-native-fontawesome": "^0.3.0",
"@fortawesome/react-native-fontawesome": "^0.3.2",
"@lingui/react": "^4.5.0",
"@mattermost/react-native-paste-input": "^0.6.4",
"@mattermost/react-native-paste-input": "^0.7.1",
"@miblanchard/react-native-slider": "^2.3.1",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-masked-view/masked-view": "0.3.0",
"@react-native-menu/menu": "^0.8.0",
"@react-native-menu/menu": "^1.1.0",
"@react-native-picker/picker": "2.6.1",
"@react-navigation/bottom-tabs": "^6.5.7",
"@react-navigation/drawer": "^6.6.2",
"@react-navigation/native": "^6.1.6",
"@react-navigation/native-stack": "^6.9.12",
"@react-navigation/bottom-tabs": "^6.5.20",
"@react-navigation/drawer": "^6.6.15",
"@react-navigation/native": "^6.1.17",
"@react-navigation/native-stack": "^6.9.26",
"@segment/analytics-next": "^1.51.3",
"@segment/analytics-react": "^1.0.0-rc1",
"@segment/analytics-react-native": "^2.10.1",
@@ -107,35 +107,36 @@
"base64-js": "^1.5.1",
"bcp-47-match": "^2.0.3",
"date-fns": "^2.30.0",
"deprecated-react-native-prop-types": "^5.0.0",
"email-validator": "^2.0.4",
"emoji-mart": "^5.5.2",
"eventemitter3": "^5.0.1",
"expo": "^50.0.17",
"expo-application": "^5.8.3",
"expo-build-properties": "^0.11.1",
"expo-camera": "~14.0.4",
"expo-clipboard": "^5.0.1",
"expo-constants": "~15.4.5",
"expo-dev-client": "~3.3.8",
"expo-device": "~5.9.3",
"expo-file-system": "^16.0.9",
"expo-haptics": "^12.8.1",
"expo-image": "~1.10.6",
"expo-image-manipulator": "^11.8.0",
"expo-image-picker": "~14.7.1",
"expo-linear-gradient": "^12.7.2",
"expo-linking": "^6.2.2",
"expo-localization": "~14.8.3",
"expo-media-library": "~15.9.1",
"expo-navigation-bar": "~2.8.1",
"expo-notifications": "~0.27.6",
"expo-sharing": "^11.10.0",
"expo-splash-screen": "~0.26.4",
"expo-status-bar": "~1.11.1",
"expo-system-ui": "~2.9.3",
"expo-task-manager": "~11.7.2",
"expo-updates": "~0.24.10",
"expo-web-browser": "~12.8.2",
"expo": "^51.0.8",
"expo-application": "^5.9.1",
"expo-build-properties": "^0.12.1",
"expo-camera": "~15.0.9",
"expo-clipboard": "^6.0.3",
"expo-constants": "~16.0.1",
"expo-dev-client": "^4.0.14",
"expo-device": "~6.0.2",
"expo-file-system": "^17.0.1",
"expo-haptics": "^13.0.1",
"expo-image": "~1.12.9",
"expo-image-manipulator": "^12.0.5",
"expo-image-picker": "~15.0.5",
"expo-linear-gradient": "^13.0.2",
"expo-linking": "^6.3.1",
"expo-localization": "~15.0.3",
"expo-media-library": "~16.0.3",
"expo-navigation-bar": "~3.0.4",
"expo-notifications": "~0.28.3",
"expo-sharing": "^12.0.1",
"expo-splash-screen": "~0.27.4",
"expo-status-bar": "~1.12.1",
"expo-system-ui": "~3.0.4",
"expo-task-manager": "~11.8.1",
"expo-updates": "~0.25.14",
"expo-web-browser": "~13.0.3",
"fast-text-encoding": "^1.0.6",
"history": "^5.3.0",
"js-sha256": "^0.9.0",
@@ -164,31 +165,31 @@
"react-compiler-runtime": "file:./lib/react-compiler-runtime",
"react-dom": "^18.2.0",
"react-keyed-flatten-children": "^3.0.0",
"react-native": "0.73.2",
"react-native": "0.74.1",
"react-native-app-clip": "bluesky-social/react-native-app-clip",
"react-native-date-picker": "^4.4.0",
"react-native-date-picker": "^4.4.2",
"react-native-drawer-layout": "^4.0.0-alpha.3",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "~2.14.0",
"react-native-gesture-handler": "~2.16.2",
"react-native-get-random-values": "~1.11.0",
"react-native-image-crop-picker": "^0.38.1",
"react-native-image-crop-picker": "0.40.3",
"react-native-ios-context-menu": "^1.15.3",
"react-native-keyboard-controller": "^1.12.1",
"react-native-pager-view": "6.2.3",
"react-native-picker-select": "^8.1.0",
"react-native-picker-select": "^9.1.3",
"react-native-progress": "bluesky-social/react-native-progress",
"react-native-reanimated": "^3.6.0",
"react-native-reanimated": "^3.11.0",
"react-native-root-siblings": "^4.1.1",
"react-native-safe-area-context": "4.8.2",
"react-native-screens": "~3.29.0",
"react-native-svg": "14.1.0",
"react-native-uitextview": "^1.1.6",
"react-native-safe-area-context": "4.10.1",
"react-native-screens": "~3.31.1",
"react-native-svg": "^15.3.0",
"react-native-uitextview": "^1.1.7",
"react-native-url-polyfill": "^1.3.0",
"react-native-uuid": "^2.0.1",
"react-native-uuid": "^2.0.2",
"react-native-view-shot": "^3.8.0",
"react-native-web": "~0.19.6",
"react-native-web": "~0.19.11",
"react-native-web-webview": "^1.0.2",
"react-native-webview": "13.6.4",
"react-native-webview": "13.10.2",
"react-responsive": "^9.0.2",
"react-textarea-autosize": "^8.5.3",
"rn-fetch-blob": "^0.12.0",
@@ -211,7 +212,7 @@
"@lingui/macro": "^4.5.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@react-native-community/eslint-config": "^3.0.0",
"@react-native/typescript-config": "^0.74.0",
"@react-native/typescript-config": "^0.74.1",
"@testing-library/jest-native": "^5.4.1",
"@testing-library/react-native": "^11.5.2",
"@tsconfig/react-native": "^2.0.3",
@@ -255,7 +256,7 @@
"jest-expo": "^50.0.1",
"jest-junit": "^15.0.0",
"lint-staged": "^13.2.3",
"metro-react-native-babel-preset": "^0.73.7",
"metro-react-native-babel-preset": "^0.74.1",
"prettier": "^2.8.3",
"react-native-dotenv": "^3.3.1",
"react-refresh": "^0.14.0",
@@ -271,7 +272,8 @@
},
"resolutions": {
"@types/react": "^18",
"**/zeed-dom": "0.10.9"
"**/zeed-dom": "0.10.9",
"@react-native/babel-preset": "0.74.1"
},
"jest": {
"preset": "jest-expo/ios",
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,16 @@
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m b/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
index e916023..0564d97 100644
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteInputView.m
@@ -22,6 +22,11 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
_backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_backedTextInputView.textInputDelegate = self;
+ // Disable inline predictions to prevent jank in the composer
+ if (@available(iOS 17.0, *)) {
+ _backedTextInputView.inlinePredictionType = UITextInlinePredictionTypeNo;
+ }
+
[self addSubview:_backedTextInputView];
}
@@ -1,56 +0,0 @@
diff --git a/node_modules/@react-navigation/native/lib/commonjs/useLinking.js b/node_modules/@react-navigation/native/lib/commonjs/useLinking.js
index ef4f368..2b0da35 100644
--- a/node_modules/@react-navigation/native/lib/commonjs/useLinking.js
+++ b/node_modules/@react-navigation/native/lib/commonjs/useLinking.js
@@ -273,8 +273,12 @@ function useLinking(ref, _ref) {
});
const currentIndex = history.index;
try {
- if (nextIndex !== -1 && nextIndex < currentIndex) {
- // An existing entry for this path exists and it's less than current index, go back to that
+ if (
+ nextIndex !== -1 &&
+ nextIndex < currentIndex &&
+ // We should only go back if the entry exists and it's less than current index
+ history.get(nextIndex - currentIndex)
+ ) { // An existing entry for this path exists and it's less than current index, go back to that
await history.go(nextIndex - currentIndex);
} else {
// We couldn't find an existing entry to go back to, so we'll go back by the delta
diff --git a/node_modules/@react-navigation/native/lib/module/useLinking.js b/node_modules/@react-navigation/native/lib/module/useLinking.js
index 62a3b43..11a5a28 100644
--- a/node_modules/@react-navigation/native/lib/module/useLinking.js
+++ b/node_modules/@react-navigation/native/lib/module/useLinking.js
@@ -264,8 +264,12 @@ export default function useLinking(ref, _ref) {
});
const currentIndex = history.index;
try {
- if (nextIndex !== -1 && nextIndex < currentIndex) {
- // An existing entry for this path exists and it's less than current index, go back to that
+ if (
+ nextIndex !== -1 &&
+ nextIndex < currentIndex &&
+ // We should only go back if the entry exists and it's less than current index
+ history.get(nextIndex - currentIndex)
+ ) { // An existing entry for this path exists and it's less than current index, go back to that
await history.go(nextIndex - currentIndex);
} else {
// We couldn't find an existing entry to go back to, so we'll go back by the delta
diff --git a/node_modules/@react-navigation/native/src/useLinking.tsx b/node_modules/@react-navigation/native/src/useLinking.tsx
index 3db40b7..9ba4ecd 100644
--- a/node_modules/@react-navigation/native/src/useLinking.tsx
+++ b/node_modules/@react-navigation/native/src/useLinking.tsx
@@ -381,7 +381,12 @@ export default function useLinking(
const currentIndex = history.index;
try {
- if (nextIndex !== -1 && nextIndex < currentIndex) {
+ if (
+ nextIndex !== -1 &&
+ nextIndex < currentIndex &&
+ // We should only go back if the entry exists and it's less than current index
+ history.get(nextIndex - currentIndex)
+ ) {
// An existing entry for this path exists and it's less than current index, go back to that
await history.go(nextIndex - currentIndex);
} else {
@@ -1,5 +0,0 @@
# React Navigation history bug patch
This patches react-navigation to fix the issues in https://github.com/bluesky-social/social-app/issues/710.
This is based on the PR found at https://github.com/react-navigation/react-navigation/pull/11833
-112
View File
@@ -1,112 +0,0 @@
diff --git a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt
index 3f50f8c..ee47fa1 100644
--- a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt
+++ b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt
@@ -33,7 +33,9 @@ import kotlin.coroutines.resumeWithException
// TODO(@bbarthec): rename to ExpoImagePicker
private const val moduleName = "ExponentImagePicker"
+
class ImagePickerModule : Module() {
+ private var isPickerOpen = false
override fun definition() = ModuleDefinition {
Name(moduleName)
@@ -129,6 +131,11 @@ class ImagePickerModule : Module() {
options: ImagePickerOptions
): Any {
return try {
+ if(isPickerOpen) {
+ return ImagePickerResponse(canceled = true)
+ }
+
+ isPickerOpen = true
var result = launchPicker(pickerLauncher)
if (
!options.allowsMultipleSelection &&
@@ -143,6 +150,8 @@ class ImagePickerModule : Module() {
mediaHandler.readExtras(result.data, options)
} catch (cause: OperationCanceledException) {
return ImagePickerResponse(canceled = true)
+ } finally {
+ isPickerOpen = false
}
}
diff --git a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
index ff15c91..9763012 100644
--- a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
+++ b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
@@ -5,12 +5,7 @@ import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.net.Uri
-import androidx.activity.result.PickVisualMediaRequest
-import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
-import androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia
import expo.modules.imagepicker.ImagePickerOptions
-import expo.modules.imagepicker.MediaTypes
-import expo.modules.imagepicker.UNLIMITED_SELECTION
import expo.modules.imagepicker.getAllDataUris
import expo.modules.imagepicker.toMediaType
import expo.modules.kotlin.activityresult.AppContextActivityResultContract
@@ -26,51 +21,26 @@ import java.io.Serializable
* @see [androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents]
*/
internal class ImageLibraryContract(
- private val appContextProvider: AppContextProvider
+ private val appContextProvider: AppContextProvider,
) : AppContextActivityResultContract<ImageLibraryContractOptions, ImagePickerContractResult> {
private val contentResolver: ContentResolver
get() = appContextProvider.appContext.reactContext?.contentResolver
?: throw Exceptions.ReactContextLost()
override fun createIntent(context: Context, input: ImageLibraryContractOptions): Intent {
- val request = PickVisualMediaRequest.Builder()
- .setMediaType(
- when (input.options.mediaTypes) {
- MediaTypes.VIDEOS -> {
- PickVisualMedia.VideoOnly
- }
-
- MediaTypes.IMAGES -> {
- PickVisualMedia.ImageOnly
- }
-
- else -> {
- PickVisualMedia.ImageAndVideo
- }
- }
- )
- .build()
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
+ .addCategory(Intent.CATEGORY_OPENABLE)
+ .setType("image/*")
if (input.options.allowsMultipleSelection) {
- val selectionLimit = input.options.selectionLimit
-
- if (selectionLimit == 1) {
- // If multiple selection is allowed but the limit is 1, we should ignore
- // the multiple selection flag and just treat it as a single selection.
- return PickVisualMedia().createIntent(context, request)
+ if(input.options.selectionLimit == 1) {
+ return intent
}
- if (selectionLimit > 1) {
- return PickMultipleVisualMedia(selectionLimit).createIntent(context, request)
- }
-
- // If the selection limit is 0, it is the same as unlimited selection.
- if (selectionLimit == UNLIMITED_SELECTION) {
- return PickMultipleVisualMedia().createIntent(context, request)
- }
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}
- return PickVisualMedia().createIntent(context, request)
+ return intent
}
override fun parseResult(input: ImageLibraryContractOptions, resultCode: Int, intent: Intent?) =
@@ -1,3 +0,0 @@
added by https://github.com/bluesky-social/social-app/pull/2384#pullrequestreview-1800985521
hackfixes the image picker on android so that the user can select from their typical image sources
@@ -1,8 +1,8 @@
diff --git a/node_modules/expo-notifications/android/build.gradle b/node_modules/expo-notifications/android/build.gradle
index 97bf4f4..6e9d427 100644
index d233e1f..cc2f856 100644
--- a/node_modules/expo-notifications/android/build.gradle
+++ b/node_modules/expo-notifications/android/build.gradle
@@ -118,6 +118,7 @@ dependencies {
@@ -32,6 +32,7 @@ dependencies {
api 'com.google.firebase:firebase-messaging:22.0.0'
api 'me.leolin:ShortcutBadger:1.1.22@aar'
@@ -46,7 +46,7 @@ index 0af7fe0..8f2c8d8 100644
try {
return payload.getString(TITLE_KEY);
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
index f1fed19..1619f59 100644
index f1fed19..80afe9e 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
@@ -20,6 +20,7 @@ import expo.modules.notifications.notifications.enums.NotificationPriority;
@@ -57,17 +57,19 @@ index f1fed19..1619f59 100644
private String mTitle;
private String mText;
private String mSubtitle;
@@ -50,6 +51,9 @@ public class NotificationContent implements Parcelable, Serializable {
@@ -50,6 +51,11 @@ public class NotificationContent implements Parcelable, Serializable {
}
};
+ @Nullable
+ public String getChannelId() { return mChannelId; }
+ public String getChannelId() {
+ return mTitle;
+ }
+
@Nullable
public String getTitle() {
return mTitle;
@@ -121,6 +125,7 @@ public class NotificationContent implements Parcelable, Serializable {
@@ -121,6 +127,7 @@ public class NotificationContent implements Parcelable, Serializable {
}
protected NotificationContent(Parcel in) {
@@ -75,7 +77,7 @@ index f1fed19..1619f59 100644
mTitle = in.readString();
mText = in.readString();
mSubtitle = in.readString();
@@ -146,6 +151,7 @@ public class NotificationContent implements Parcelable, Serializable {
@@ -146,6 +153,7 @@ public class NotificationContent implements Parcelable, Serializable {
@Override
public void writeToParcel(Parcel dest, int flags) {
@@ -83,7 +85,7 @@ index f1fed19..1619f59 100644
dest.writeString(mTitle);
dest.writeString(mText);
dest.writeString(mSubtitle);
@@ -166,6 +172,7 @@ public class NotificationContent implements Parcelable, Serializable {
@@ -166,6 +174,7 @@ public class NotificationContent implements Parcelable, Serializable {
private static final long serialVersionUID = 397666843266836802L;
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
@@ -91,7 +93,7 @@ index f1fed19..1619f59 100644
out.writeObject(mTitle);
out.writeObject(mText);
out.writeObject(mSubtitle);
@@ -190,6 +197,7 @@ public class NotificationContent implements Parcelable, Serializable {
@@ -190,6 +199,7 @@ public class NotificationContent implements Parcelable, Serializable {
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
@@ -99,7 +101,7 @@ index f1fed19..1619f59 100644
mTitle = (String) in.readObject();
mText = (String) in.readObject();
mSubtitle = (String) in.readObject();
@@ -240,6 +248,7 @@ public class NotificationContent implements Parcelable, Serializable {
@@ -240,6 +250,7 @@ public class NotificationContent implements Parcelable, Serializable {
}
public static class Builder {
@@ -107,7 +109,7 @@ index f1fed19..1619f59 100644
private String mTitle;
private String mText;
private String mSubtitle;
@@ -260,6 +269,11 @@ public class NotificationContent implements Parcelable, Serializable {
@@ -260,6 +271,11 @@ public class NotificationContent implements Parcelable, Serializable {
useDefaultVibrationPattern();
}
@@ -119,7 +121,7 @@ index f1fed19..1619f59 100644
public Builder setTitle(String title) {
mTitle = title;
return this;
@@ -336,6 +350,7 @@ public class NotificationContent implements Parcelable, Serializable {
@@ -336,6 +352,7 @@ public class NotificationContent implements Parcelable, Serializable {
public NotificationContent build() {
NotificationContent content = new NotificationContent();
@@ -128,18 +130,10 @@ index f1fed19..1619f59 100644
content.mSubtitle = mSubtitle;
content.mText = mText;
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java
index 6bd9928..aab71ea 100644
index 6bd9928..ee93d70 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java
@@ -7,7 +7,6 @@ import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
-import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.provider.Settings;
@@ -48,6 +47,10 @@ public class ExpoNotificationBuilder extends ChannelAwareNotificationBuilder {
@@ -48,6 +48,10 @@ public class ExpoNotificationBuilder extends ChannelAwareNotificationBuilder {
NotificationContent content = getNotificationContent();
@@ -1,11 +1,11 @@
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)
diff --git a/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift b/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift
index b85291e..07a5d3c 100644
--- a/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift
+++ b/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift
@@ -78,13 +78,20 @@ public final class ExpoUpdatesUpdate: Update {
status = UpdateStatus.StatusPending
}
+ // 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()
@@ -23,4 +23,4 @@ index 189a5f5..8d5b8e6 100644
+ commitTime: date,
runtimeVersion: runtimeVersion,
keep: true,
status: UpdateStatus.StatusPending,
status: status,
@@ -4,4 +4,4 @@ This is a small patch to convert timestamp formats that are returned from the ba
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.
Don't remove unless we make changes on the backend to support both platforms.
@@ -0,0 +1,237 @@
diff --git a/node_modules/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts b/node_modules/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts
index 92ebe62..5f8207e 100644
--- a/node_modules/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts
+++ b/node_modules/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts
@@ -96,7 +96,8 @@ export interface AnimatedComponentRef extends Component {
export interface IAnimatedComponentInternal {
_styles: StyleProps[] | null;
_animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
- _viewTag: number;
+ _componentViewTag: number;
+ _eventViewTag: number;
_isFirstRender: boolean;
jestAnimatedStyle: { value: StyleProps };
_component: AnimatedComponentRef | HTMLElement | null;
diff --git a/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx b/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
index 88b3fdf..2488ebc 100644
--- a/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
+++ b/node_modules/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx
@@ -122,7 +122,8 @@ export function createAnimatedComponent(
{
_styles: StyleProps[] | null = null;
_animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
- _viewTag = -1;
+ _componentViewTag = -1;
+ _eventViewTag = -1;
_isFirstRender = true;
jestAnimatedStyle: { value: StyleProps } = { value: {} };
_component: AnimatedComponentRef | HTMLElement | null = null;
@@ -143,7 +144,8 @@ export function createAnimatedComponent(
}
componentDidMount() {
- this._viewTag = this._getViewInfo().viewTag as number;
+ this._setComponentViewTag();
+ this._setEventViewTag();
this._attachNativeEvents();
this._jsPropsUpdater.addOnJSPropsChangeListener(this);
this._attachAnimatedStyles();
@@ -185,7 +187,10 @@ export function createAnimatedComponent(
if (this.props.sharedTransitionTag) {
this._configureSharedTransition(true);
}
- this._sharedElementTransition?.unregisterTransition(this._viewTag, true);
+ this._sharedElementTransition?.unregisterTransition(
+ this._componentViewTag,
+ true
+ );
const exiting = this.props.exiting;
if (
@@ -209,7 +214,7 @@ export function createAnimatedComponent(
: getReduceMotionFromConfig();
if (!reduceMotionInExiting) {
updateLayoutAnimations(
- this._viewTag,
+ this._componentViewTag,
LayoutAnimationType.EXITING,
maybeBuild(
exiting,
@@ -221,12 +226,22 @@ export function createAnimatedComponent(
}
}
- _getEventViewRef() {
- // Make sure to get the scrollable node for components that implement
- // `ScrollResponder.Mixin`.
- return (this._component as AnimatedComponentRef)?.getScrollableNode
- ? (this._component as AnimatedComponentRef).getScrollableNode?.()
- : this._component;
+ _setComponentViewTag() {
+ this._componentViewTag = this._getViewInfo().viewTag as number;
+ }
+
+ _setEventViewTag() {
+ // Setting the tag for registering events - since the event emitting view can be nested inside the main component
+ const componentAnimatedRef = this._component as AnimatedComponentRef;
+ if (componentAnimatedRef.getScrollableNode) {
+ const scrollableNode = componentAnimatedRef.getScrollableNode();
+ this._eventViewTag = findNodeHandle(scrollableNode) ?? -1;
+ } else {
+ this._eventViewTag =
+ findNodeHandle(
+ options?.setNativeProps ? this : componentAnimatedRef
+ ) ?? -1;
+ }
}
_attachNativeEvents() {
@@ -236,7 +251,7 @@ export function createAnimatedComponent(
has('workletEventHandler', prop) &&
prop.workletEventHandler instanceof WorkletEventHandler
) {
- prop.workletEventHandler.registerForEvents(this._viewTag, key);
+ prop.workletEventHandler.registerForEvents(this._eventViewTag, key);
}
}
}
@@ -248,7 +263,7 @@ export function createAnimatedComponent(
has('workletEventHandler', prop) &&
prop.workletEventHandler instanceof WorkletEventHandler
) {
- prop.workletEventHandler.unregisterFromEvents(this._viewTag);
+ prop.workletEventHandler.unregisterFromEvents(this._eventViewTag);
}
}
}
@@ -258,15 +273,17 @@ export function createAnimatedComponent(
for (const style of this._styles) {
style.viewsRef.remove(this);
}
- } else if (this._viewTag !== -1 && this._styles !== null) {
+ } else if (this._componentViewTag !== -1 && this._styles !== null) {
for (const style of this._styles) {
- style.viewDescriptors.remove(this._viewTag);
+ style.viewDescriptors.remove(this._componentViewTag);
}
if (this.props.animatedProps?.viewDescriptors) {
- this.props.animatedProps.viewDescriptors.remove(this._viewTag);
+ this.props.animatedProps.viewDescriptors.remove(
+ this._componentViewTag
+ );
}
if (isFabric()) {
- removeFromPropsRegistry(this._viewTag);
+ removeFromPropsRegistry(this._componentViewTag);
}
}
}
@@ -283,15 +300,19 @@ export function createAnimatedComponent(
const newProp = this.props[key];
if (!newProp) {
// Prop got deleted
- prevProp.workletEventHandler.unregisterFromEvents(this._viewTag);
+ prevProp.workletEventHandler.unregisterFromEvents(
+ this._eventViewTag
+ );
} else if (
has('workletEventHandler', newProp) &&
newProp.workletEventHandler instanceof WorkletEventHandler &&
newProp.workletEventHandler !== prevProp.workletEventHandler
) {
// Prop got changed
- prevProp.workletEventHandler.unregisterFromEvents(this._viewTag);
- newProp.workletEventHandler.registerForEvents(this._viewTag);
+ prevProp.workletEventHandler.unregisterFromEvents(
+ this._eventViewTag
+ );
+ newProp.workletEventHandler.registerForEvents(this._eventViewTag);
}
}
}
@@ -304,7 +325,7 @@ export function createAnimatedComponent(
!prevProps[key]
) {
// Prop got added
- newProp.workletEventHandler.registerForEvents(this._viewTag);
+ newProp.workletEventHandler.registerForEvents(this._eventViewTag);
}
}
}
@@ -381,7 +402,7 @@ export function createAnimatedComponent(
adaptViewConfig(viewConfig);
}
- this._viewTag = viewTag as number;
+ this._componentViewTag = viewTag as number;
// remove old styles
if (prevStyles) {
@@ -487,7 +508,11 @@ export function createAnimatedComponent(
AnimatedComponent.displayName
)
: undefined;
- updateLayoutAnimations(this._viewTag, LayoutAnimationType.LAYOUT, layout);
+ updateLayoutAnimations(
+ this._componentViewTag,
+ LayoutAnimationType.LAYOUT,
+ layout
+ );
}
_configureSharedTransition(isUnmounting = false) {
@@ -497,7 +522,7 @@ export function createAnimatedComponent(
const { sharedTransitionTag } = this.props;
if (!sharedTransitionTag) {
this._sharedElementTransition?.unregisterTransition(
- this._viewTag,
+ this._componentViewTag,
isUnmounting
);
this._sharedElementTransition = null;
@@ -508,7 +533,7 @@ export function createAnimatedComponent(
this._sharedElementTransition ??
new SharedTransition();
sharedElementTransition.registerTransition(
- this._viewTag,
+ this._componentViewTag,
sharedTransitionTag,
isUnmounting
);
@@ -527,7 +552,7 @@ export function createAnimatedComponent(
? (ref as HTMLElement)
: findNodeHandle(ref as Component);
- this._viewTag = tag as number;
+ this._componentViewTag = tag as number;
const { layout, entering, exiting, sharedTransitionTag } = this.props;
if (
diff --git a/node_modules/react-native-reanimated/lib/module/reanimated2/index.js b/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
index ac9be5d..86d4605 100644
--- a/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
+++ b/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
@@ -47,4 +47,5 @@ export { LayoutAnimationConfig } from './component/LayoutAnimationConfig';
export { PerformanceMonitor } from './component/PerformanceMonitor';
export { startMapper, stopMapper } from './mappers';
export { startScreenTransition, finishScreenTransition, ScreenTransition } from './screenTransition';
+export { isReducedMotion } from './PlatformChecker';
//# sourceMappingURL=index.js.map
diff --git a/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts b/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
index f01dc57..161ef22 100644
--- a/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
+++ b/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
@@ -36,3 +36,4 @@ export type { FlatListPropsWithLayout } from './component/FlatList';
export { startMapper, stopMapper } from './mappers';
export { startScreenTransition, finishScreenTransition, ScreenTransition, } from './screenTransition';
export type { AnimatedScreenTransition, GoBackGesture, ScreenTransitionConfig, } from './screenTransition';
+export { isReducedMotion } from './PlatformChecker';
diff --git a/node_modules/react-native-reanimated/src/reanimated2/index.ts b/node_modules/react-native-reanimated/src/reanimated2/index.ts
index 5885fa1..a3c693f 100644
--- a/node_modules/react-native-reanimated/src/reanimated2/index.ts
+++ b/node_modules/react-native-reanimated/src/reanimated2/index.ts
@@ -284,3 +284,4 @@ export type {
GoBackGesture,
ScreenTransitionConfig,
} from './screenTransition';
+export { isReducedMotion } from './PlatformChecker';
@@ -1,30 +0,0 @@
diff --git a/node_modules/react-native-reanimated/lib/module/reanimated2/index.js b/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
index 91e49f4..c10d3fc 100644
--- a/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
+++ b/node_modules/react-native-reanimated/lib/module/reanimated2/index.js
@@ -45,4 +45,5 @@ export { getUseOfValueInStyleWarning } from './pluginUtils';
export { withReanimatedTimer, advanceAnimationByTime, advanceAnimationByFrame, setUpTests, getAnimatedStyle } from './jestUtils';
export { LayoutAnimationConfig } from './component/LayoutAnimationConfig';
export { startMapper, stopMapper } from './mappers';
+export { isReducedMotion } from './PlatformChecker';
//# sourceMappingURL=index.js.map
\ No newline at end of file
diff --git a/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts b/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
index 96bd913..ad63a09 100644
--- a/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
+++ b/node_modules/react-native-reanimated/lib/typescript/reanimated2/index.d.ts
@@ -33,3 +33,4 @@ export type { Adaptable, AdaptTransforms, AnimateProps, AnimatedProps, AnimatedT
export type { AnimatedScrollViewProps } from './component/ScrollView';
export type { FlatListPropsWithLayout } from './component/FlatList';
export { startMapper, stopMapper } from './mappers';
+export { isReducedMotion } from './PlatformChecker';
diff --git a/node_modules/react-native-reanimated/src/reanimated2/index.ts b/node_modules/react-native-reanimated/src/reanimated2/index.ts
index 096dc05..38fc01d 100644
--- a/node_modules/react-native-reanimated/src/reanimated2/index.ts
+++ b/node_modules/react-native-reanimated/src/reanimated2/index.ts
@@ -271,3 +271,4 @@ export type {
export type { AnimatedScrollViewProps } from './component/ScrollView';
export type { FlatListPropsWithLayout } from './component/FlatList';
export { startMapper, stopMapper } from './mappers';
+export { isReducedMotion } from './PlatformChecker';
\ No newline at end of file
@@ -0,0 +1,17 @@
const {withAndroidManifest} = require('expo/config-plugins')
module.exports = function withAndroidManifestLaunchModePlugin(appConfig) {
return withAndroidManifest(appConfig, function (decoratedAppConfig) {
try {
const mainApplication =
decoratedAppConfig.modResults.manifest.application[0]
const mainActivity = mainApplication.activity.find(
elem => elem.$['android:name'] === '.MainActivity',
)
mainActivity.$['android:launchMode'] = 'singleTop'
} catch (e) {
console.error(`withAndroidManifestLaunchModePlugin failed`, e)
}
return decoratedAppConfig
})
}
+1 -1
View File
@@ -143,7 +143,7 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<KeyboardProvider enabled={true}>
<KeyboardProvider enabled={true} statusBarTranslucent={true}>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
+14 -13
View File
@@ -353,11 +353,11 @@ function HomeTabNavigator() {
return (
<HomeTab.Navigator
screenOptions={{
animation: isAndroid ? 'none' : undefined,
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
animationDuration: 250,
contentStyle: pal.view,
}}>
<HomeTab.Screen name="Home" getComponent={() => HomeScreen} />
@@ -371,11 +371,11 @@ function SearchTabNavigator() {
return (
<SearchTab.Navigator
screenOptions={{
animation: isAndroid ? 'none' : undefined,
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
animationDuration: 250,
contentStyle: pal.view,
}}>
<SearchTab.Screen name="Search" getComponent={() => SearchScreen} />
@@ -389,11 +389,11 @@ function FeedsTabNavigator() {
return (
<FeedsTab.Navigator
screenOptions={{
animation: isAndroid ? 'none' : undefined,
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
animationDuration: 250,
contentStyle: pal.view,
}}>
<FeedsTab.Screen name="Feeds" getComponent={() => FeedsScreen} />
@@ -407,11 +407,11 @@ function NotificationsTabNavigator() {
return (
<NotificationsTab.Navigator
screenOptions={{
animation: isAndroid ? 'none' : undefined,
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
animationDuration: 250,
contentStyle: pal.view,
}}>
<NotificationsTab.Screen
@@ -429,11 +429,11 @@ function MyProfileTabNavigator() {
return (
<MyProfileTab.Navigator
screenOptions={{
animation: isAndroid ? 'none' : undefined,
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
animationDuration: 250,
contentStyle: pal.view,
}}>
<MyProfileTab.Screen
@@ -454,11 +454,11 @@ function MessagesTabNavigator() {
return (
<MessagesTab.Navigator
screenOptions={{
animation: isAndroid ? 'none' : undefined,
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
animationDuration: 250,
contentStyle: pal.view,
}}>
<MessagesTab.Screen
@@ -488,10 +488,11 @@ const FlatNavigator = () => {
<Flat.Navigator
screenListeners={screenListeners}
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
animationDuration: 250,
contentStyle: pal.view,
}}>
<Flat.Screen
+24 -7
View File
@@ -1,7 +1,8 @@
import {Platform} from 'react-native'
import {Platform, StyleSheet} from 'react-native'
import * as tokens from '#/alf/tokens'
import {native, web} from '#/alf/util/platform'
import hairlineWidth = StyleSheet.hairlineWidth
export const atoms = {
/*
@@ -55,6 +56,13 @@ export const atoms = {
height: '100vh',
}),
/*
* Theme-independent bg colors
*/
bg_transparent: {
backgroundColor: 'transparent',
},
/*
* Border radius
*/
@@ -270,19 +278,19 @@ export const atoms = {
borderWidth: 0,
},
border: {
borderWidth: 1,
borderWidth: hairlineWidth,
},
border_t: {
borderTopWidth: 1,
borderTopWidth: hairlineWidth,
},
border_b: {
borderBottomWidth: 1,
borderBottomWidth: hairlineWidth,
},
border_l: {
borderLeftWidth: 1,
borderLeftWidth: hairlineWidth,
},
border_r: {
borderRightWidth: 1,
borderRightWidth: hairlineWidth,
},
/*
@@ -841,7 +849,7 @@ export const atoms = {
marginRight: 'auto',
},
/*
* Pointer events
* Pointer events & user select
*/
pointer_events_none: {
pointerEvents: 'none',
@@ -849,6 +857,15 @@ export const atoms = {
pointer_events_auto: {
pointerEvents: 'auto',
},
user_select_none: {
userSelect: 'none',
},
user_select_text: {
userSelect: 'text',
},
user_select_all: {
userSelect: 'all',
},
/*
* Text decoration
*/
+5 -3
View File
@@ -1,12 +1,14 @@
import React from 'react'
import {Dimensions} from 'react-native'
import * as themes from '#/alf/themes'
export * from '#/alf/types'
export * as tokens from '#/alf/tokens'
export {atoms} from '#/alf/atoms'
export * from '#/alf/util/platform'
export * as tokens from '#/alf/tokens'
export * from '#/alf/types'
export * from '#/alf/util/flatten'
export * from '#/alf/util/platform'
export * from '#/alf/util/themeSelector'
type BreakpointName = keyof typeof breakpoints
+5 -5
View File
@@ -1,11 +1,11 @@
import {atoms} from '#/alf/atoms'
import * as tokens from '#/alf/tokens'
import type {Mutable} from '#/alf/types'
import {atoms} from '#/alf/atoms'
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
export type ThemeName = 'light' | 'dim' | 'dark'
export type ReadonlyTheme = typeof light
export type Theme = Mutable<ReadonlyTheme>
export type Theme = Mutable<ReadonlyTheme> & {name: ThemeName}
export type ReadonlyPalette = typeof lightPalette
export type Palette = Mutable<ReadonlyPalette>
@@ -193,7 +193,7 @@ export const dimPalette: Palette = {
} as const
export const light = {
name: 'light',
name: 'light' as ThemeName,
palette: lightPalette,
atoms: {
text: {
@@ -278,7 +278,7 @@ export const light = {
}
export const dark: Theme = {
name: 'dark',
name: 'dark' as ThemeName,
palette: darkPalette,
atoms: {
text: {
@@ -367,7 +367,7 @@ export const dark: Theme = {
export const dim: Theme = {
...dark,
name: 'dim',
name: 'dim' as ThemeName,
palette: dimPalette,
atoms: {
...dark.atoms,
+14
View File
@@ -0,0 +1,14 @@
import {ThemeName} from '#/alf/themes'
export function select<T>(name: ThemeName, options: Record<ThemeName, T>) {
switch (name) {
case 'light':
return options.light
case 'dark':
return options.dark || options.dim
case 'dim':
return options.dim || options.dark
default:
throw new Error(`select(theme, options) received unknown theme ${name}`)
}
}
+17 -15
View File
@@ -1,25 +1,27 @@
import React from 'react'
import {ColorSchemeName, useColorScheme} from 'react-native'
import {useThemePrefs} from 'state/shell'
import {isWeb} from 'platform/detection'
import {ThemeName, light, dark, dim} from '#/alf/themes'
import * as SystemUI from 'expo-system-ui'
import {isWeb} from 'platform/detection'
import {useThemePrefs} from 'state/shell'
import {dark, dim, light, ThemeName} from '#/alf/themes'
export function useColorModeTheme(): ThemeName {
const theme = useThemeName()
React.useLayoutEffect(() => {
updateDocument(theme)
SystemUI.setBackgroundColorAsync(getBackgroundColor(theme))
}, [theme])
return theme
}
export function useThemeName(): ThemeName {
const colorScheme = useColorScheme()
const {colorMode, darkTheme} = useThemePrefs()
React.useLayoutEffect(() => {
const theme = getThemeName(colorScheme, colorMode, darkTheme)
updateDocument(theme)
SystemUI.setBackgroundColorAsync(getBackgroundColor(theme))
}, [colorMode, colorScheme, darkTheme])
return React.useMemo(
() => getThemeName(colorScheme, colorMode, darkTheme),
[colorScheme, colorMode, darkTheme],
)
return getThemeName(colorScheme, colorMode, darkTheme)
}
function getThemeName(
@@ -53,7 +55,7 @@ function updateDocument(theme: ThemeName) {
}
}
function getBackgroundColor(theme: ThemeName): string {
export function getBackgroundColor(theme: ThemeName): string {
switch (theme) {
case 'light':
return light.atoms.bg.backgroundColor
+2 -2
View File
@@ -37,7 +37,7 @@ export function AccountList({
style={[
a.rounded_md,
a.overflow_hidden,
a.border,
{borderWidth: 1},
t.atoms.border_contrast_low,
]}>
{accounts.map(account => (
@@ -48,7 +48,7 @@ export function AccountList({
isCurrentAccount={account.did === currentAccount?.did}
isPendingAccount={account.did === pendingDid}
/>
<View style={[a.border_b, t.atoms.border_contrast_low]} />
<View style={[{borderBottomWidth: 1}, t.atoms.border_contrast_low]} />
</React.Fragment>
))}
<Button
+11 -3
View File
@@ -28,7 +28,7 @@ export type ButtonColor =
| 'gradient_sunset'
| 'gradient_nordic'
| 'gradient_bonfire'
export type ButtonSize = 'tiny' | 'small' | 'medium' | 'large'
export type ButtonSize = 'tiny' | 'xsmall' | 'small' | 'medium' | 'large'
export type ButtonShape = 'round' | 'square' | 'default'
export type VariantProps = {
/**
@@ -64,13 +64,14 @@ type NonTextElements =
export type ButtonProps = Pick<
PressableProps,
'disabled' | 'onPress' | 'testID' | 'onLongPress'
'disabled' | 'onPress' | 'testID' | 'onLongPress' | 'hitSlop'
> &
AccessibilityProps &
VariantProps & {
testID?: string
label: string
style?: StyleProp<ViewStyle>
hoverStyle?: StyleProp<ViewStyle>
children: NonTextElements | ((context: ButtonContext) => NonTextElements)
}
@@ -96,6 +97,7 @@ export function Button({
label,
disabled = false,
style,
hoverStyle: hoverStyleProp,
...rest
}: ButtonProps) {
const t = useTheme()
@@ -281,6 +283,8 @@ export function Button({
baseStyles.push({paddingVertical: 12}, a.px_2xl, a.rounded_sm, a.gap_md)
} else if (size === 'small') {
baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
} else if (size === 'xsmall') {
baseStyles.push({paddingVertical: 6}, a.px_sm, a.rounded_sm, a.gap_sm)
} else if (size === 'tiny') {
baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs)
}
@@ -293,6 +297,8 @@ export function Button({
}
} else if (size === 'small') {
baseStyles.push({height: 34, width: 34})
} else if (size === 'xsmall') {
baseStyles.push({height: 28, width: 28})
} else if (size === 'tiny') {
baseStyles.push({height: 20, width: 20})
}
@@ -374,8 +380,10 @@ export function Button({
a.align_center,
a.justify_center,
flattenedBaseStyles,
...(state.hovered || state.pressed ? hoverStyles : []),
flatten(style),
...(state.hovered || state.pressed
? [hoverStyles, flatten(hoverStyleProp)]
: []),
]}
onPressIn={onPressIn}
onPressOut={onPressOut}
+44 -41
View File
@@ -32,6 +32,7 @@ import {
DialogOuterProps,
} from '#/components/Dialog/types'
import {createInput} from '#/components/forms/TextField'
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
import {Portal} from '#/components/Portal'
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
@@ -170,47 +171,49 @@ export function Outer({
return (
isOpen && (
<Portal>
<View
// iOS
accessibilityViewIsModal
// Android
importantForAccessibility="yes"
style={[a.absolute, a.inset_0]}
testID={testID}
onTouchMove={() => Keyboard.dismiss()}>
<BottomSheet
enableDynamicSizing={!hasSnapPoints}
enablePanDownToClose
keyboardBehavior="interactive"
android_keyboardInputMode="adjustResize"
keyboardBlurBehavior="restore"
topInset={insets.top}
{...sheetOptions}
snapPoints={sheetOptions.snapPoints || ['100%']}
ref={sheet}
index={openIndex}
backgroundStyle={{backgroundColor: 'transparent'}}
backdropComponent={Backdrop}
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
handleStyle={{display: 'none'}}
onClose={onCloseAnimationComplete}>
<Context.Provider value={context}>
<View
style={[
a.absolute,
a.inset_0,
t.atoms.bg,
{
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
height: Dimensions.get('window').height * 2,
},
]}
/>
{children}
</Context.Provider>
</BottomSheet>
</View>
<FullWindowOverlay>
<View
// iOS
accessibilityViewIsModal
// Android
importantForAccessibility="yes"
style={[a.absolute, a.inset_0]}
testID={testID}
onTouchMove={() => Keyboard.dismiss()}>
<BottomSheet
enableDynamicSizing={!hasSnapPoints}
enablePanDownToClose
keyboardBehavior="interactive"
android_keyboardInputMode="adjustResize"
keyboardBlurBehavior="restore"
topInset={insets.top}
{...sheetOptions}
snapPoints={sheetOptions.snapPoints || ['100%']}
ref={sheet}
index={openIndex}
backgroundStyle={{backgroundColor: 'transparent'}}
backdropComponent={Backdrop}
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
handleStyle={{display: 'none'}}
onClose={onCloseAnimationComplete}>
<Context.Provider value={context}>
<View
style={[
a.absolute,
a.inset_0,
t.atoms.bg,
{
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
height: Dimensions.get('window').height * 2,
},
]}
/>
{children}
</Context.Provider>
</BottomSheet>
</View>
</FullWindowOverlay>
</Portal>
)
)
+1
View File
@@ -0,0 +1 @@
export {FullWindowOverlay} from 'react-native-screens'
+1
View File
@@ -0,0 +1 @@
export {Fragment as FullWindowOverlay} from 'react'
+3 -3
View File
@@ -36,7 +36,7 @@ export function SubmitView({
}) {
const t = useTheme()
const {_} = useLingui()
const {getAgent} = useAgent()
const agent = useAgent()
const [details, setDetails] = React.useState<string>('')
const [submitting, setSubmitting] = React.useState<boolean>(false)
const [selectedServices, setSelectedServices] = React.useState<string[]>([
@@ -62,7 +62,7 @@ export function SubmitView({
}
const results = await Promise.all(
selectedServices.map(did =>
getAgent()
agent
.withProxy('atproto_labeler', did)
.createModerationReport(report)
.then(
@@ -92,7 +92,7 @@ export function SubmitView({
selectedServices,
onSubmitComplete,
setError,
getAgent,
agent,
])
return (
+13 -7
View File
@@ -28,6 +28,7 @@ export function RichText({
authorHandle,
onLinkPress,
interactiveStyle,
emojiMultiplier = 1.85,
}: TextStyleProp &
Pick<TextProps, 'selectable'> & {
value: RichTextAPI | string
@@ -38,6 +39,7 @@ export function RichText({
authorHandle?: string
onLinkPress?: LinkProps['onPress']
interactiveStyle?: TextStyle
emojiMultiplier?: number
}) {
const richText = React.useMemo(
() =>
@@ -57,17 +59,14 @@ export function RichText({
const {text, facets} = richText
if (!facets?.length) {
if (text.length <= 5 && /^\p{Extended_Pictographic}+$/u.test(text)) {
if (isOnlyEmoji(text)) {
const fontSize =
(flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
return (
<Text
selectable={selectable}
testID={testID}
style={[
{
fontSize: 26,
lineHeight: 30,
},
]}
style={[plainStyles, {fontSize}]}
// @ts-ignore web only -prf
dataSet={WORD_WRAP}>
{text}
@@ -247,3 +246,10 @@ function RichTextTag({
</React.Fragment>
)
}
export function isOnlyEmoji(text: string) {
return (
text.length <= 15 &&
/^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u.test(text)
)
}
+5 -3
View File
@@ -26,9 +26,11 @@ import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck'
import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX'
import {
Person_Stroke2_Corner0_Rounded as Person,
PersonCheck_Stroke2_Corner0_Rounded as PersonCheck,
PersonX_Stroke2_Corner0_Rounded as PersonX,
} from '#/components/icons/Person'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
import * as Menu from '#/components/Menu'
import * as Prompt from '#/components/Prompt'
+47 -33
View File
@@ -6,7 +6,11 @@ import {
TextStyle,
View,
} from 'react-native'
import {ChatBskyConvoDefs, RichText as RichTextAPI} from '@atproto/api'
import {
AppBskyEmbedRecord,
ChatBskyConvoDefs,
RichText as RichTextAPI,
} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -17,7 +21,8 @@ import {atoms as a, useTheme} from '#/alf'
import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {RichText} from '../RichText'
import {isOnlyEmoji, RichText} from '../RichText'
import {MessageItemEmbed} from './MessageItemEmbed'
let MessageItem = ({
item,
@@ -77,37 +82,46 @@ let MessageItem = ({
return (
<View style={[isFromSelf ? a.mr_md : a.ml_md]}>
<ActionsWrapper isFromSelf={isFromSelf} message={message}>
<View
style={[
a.py_sm,
a.my_2xs,
a.rounded_md,
{
paddingLeft: 14,
paddingRight: 14,
backgroundColor: isFromSelf
? isPending
? pendingColor
: t.palette.primary_500
: t.palette.contrast_50,
borderRadius: 17,
},
isFromSelf
? {borderBottomRightRadius: isLastInGroup ? 2 : 17}
: {borderBottomLeftRadius: isLastInGroup ? 2 : 17},
]}>
<RichText
value={rt}
style={[
a.text_md,
a.leading_snug,
isFromSelf && {color: t.palette.white},
isPending && t.name !== 'light' && {color: t.palette.primary_300},
]}
interactiveStyle={a.underline}
enableTags
/>
</View>
{AppBskyEmbedRecord.isView(message.embed) && (
<MessageItemEmbed embed={message.embed} />
)}
{rt.text.length > 0 && (
<View
style={
!isOnlyEmoji(message.text) && [
a.py_sm,
a.my_2xs,
a.rounded_md,
{
paddingLeft: 14,
paddingRight: 14,
backgroundColor: isFromSelf
? isPending
? pendingColor
: t.palette.primary_500
: t.palette.contrast_50,
borderRadius: 17,
},
isFromSelf ? a.self_end : a.self_start,
isFromSelf
? {borderBottomRightRadius: isLastInGroup ? 2 : 17}
: {borderBottomLeftRadius: isLastInGroup ? 2 : 17},
]
}>
<RichText
value={rt}
style={[
a.text_md,
isFromSelf && {color: t.palette.white},
isPending &&
t.name !== 'light' && {color: t.palette.primary_300},
]}
interactiveStyle={a.underline}
enableTags
emojiMultiplier={3}
/>
</View>
)}
</ActionsWrapper>
{isLastInGroup && (
+24
View File
@@ -0,0 +1,24 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyEmbedRecord} from '@atproto/api'
import {isNative} from '#/platform/detection'
import {PostEmbeds} from '#/view/com/util/post-embeds'
import {atoms as a, useTheme} from '#/alf'
let MessageItemEmbed = ({
embed,
}: {
embed: AppBskyEmbedRecord.View
}): React.ReactNode => {
const t = useTheme()
return (
<View
style={[a.my_xs, t.atoms.bg, a.rounded_md, isNative && {flexBasis: 0}]}>
<PostEmbeds embed={embed} />
</View>
)
}
MessageItemEmbed = React.memo(MessageItemEmbed)
export {MessageItemEmbed}
+36 -11
View File
@@ -6,12 +6,16 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {getTranslatorLink} from '#/locale/helpers'
import {useLanguagePrefs} from '#/state/preferences'
import {useOpenLink} from '#/state/preferences/in-app-browser'
import {isWeb} from 'platform/detection'
import {useConvoActive} from 'state/messages/convo'
import {useSession} from 'state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
import {ReportDialog} from '#/components/dms/ReportDialog'
import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
@@ -35,10 +39,12 @@ export let MessageMenu = ({
const convo = useConvoActive()
const deleteControl = usePromptControl()
const reportControl = usePromptControl()
const langPrefs = useLanguagePrefs()
const openLink = useOpenLink()
const isFromSelf = message.sender?.did === currentAccount?.did
const onCopyPostText = React.useCallback(() => {
const onCopyMessage = React.useCallback(() => {
const str = richTextToString(
new RichText({
text: message.text,
@@ -51,6 +57,14 @@ export let MessageMenu = ({
Toast.show(_(msg`Copied to clipboard`))
}, [_, message.text, message.facets])
const onPressTranslateMessage = React.useCallback(() => {
const translatorUrl = getTranslatorLink(
message.text,
langPrefs.primaryLanguage,
)
openLink(translatorUrl)
}, [langPrefs.primaryLanguage, message.text, openLink])
const onDelete = React.useCallback(() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
convo
@@ -81,16 +95,27 @@ export let MessageMenu = ({
)}
<Menu.Outer>
<Menu.Group>
<Menu.Item
testID="messageDropdownCopyBtn"
label={_(msg`Copy message text`)}
onPress={onCopyPostText}>
<Menu.ItemText>{_(msg`Copy message text`)}</Menu.ItemText>
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
</Menu.Item>
</Menu.Group>
<Menu.Divider />
{message.text.length > 0 && (
<>
<Menu.Group>
<Menu.Item
testID="messageDropdownTranslateBtn"
label={_(msg`Translate`)}
onPress={onPressTranslateMessage}>
<Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText>
<Menu.ItemIcon icon={Translate} position="right" />
</Menu.Item>
<Menu.Item
testID="messageDropdownCopyBtn"
label={_(msg`Copy message text`)}
onPress={onCopyMessage}>
<Menu.ItemText>{_(msg`Copy message text`)}</Menu.ItemText>
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
</Menu.Item>
</Menu.Group>
<Menu.Divider />
</>
)}
<Menu.Group>
<Menu.Item
testID="messageDropdownDeleteBtn"
+64 -43
View File
@@ -22,6 +22,7 @@ import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {ConvoMenu} from '#/components/dms/ConvoMenu'
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
import {Link} from '#/components/Link'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import {Text} from '#/components/Typography'
const PFP_SIZE = isWeb ? 40 : 34
@@ -58,7 +59,7 @@ export let MessagesListHeader = ({
t.atoms.border_contrast_low,
a.border_b,
a.flex_row,
a.align_center,
a.align_start,
a.gap_sm,
gtTablet ? a.pl_lg : a.pl_xl,
a.pr_lg,
@@ -69,7 +70,7 @@ export let MessagesListHeader = ({
testID="conversationHeaderBackBtn"
onPress={onPressBack}
hitSlop={BACK_HITSLOP}
style={{width: 30, height: 30}}
style={{width: 30, height: 30, marginTop: isWeb ? 6 : 4}}
accessibilityRole="button"
accessibilityLabel={_(msg`Back`)}
accessibilityHint="">
@@ -152,51 +153,71 @@ function HeaderReady({
)
return (
<>
<Link
style={[a.flex_row, a.align_center, a.gap_md, a.flex_1, a.pr_md]}
to={makeProfileLink(profile)}>
<PreviewableUserAvatar
size={PFP_SIZE}
profile={profile}
moderation={moderation.ui('avatar')}
disableHoverCard={moderation.blocked}
/>
<View style={a.flex_1}>
<Text
style={[a.text_md, a.font_bold, web(a.leading_normal)]}
numberOfLines={1}>
{displayName}
</Text>
{!isDeletedAccount && (
<View style={[a.flex_1]}>
<View style={[a.w_full, a.flex_row, a.align_center, a.justify_between]}>
<Link
style={[a.flex_row, a.align_start, a.gap_md, a.flex_1, a.pr_md]}
to={makeProfileLink(profile)}>
<View style={[a.pt_2xs]}>
<PreviewableUserAvatar
size={PFP_SIZE}
profile={profile}
moderation={moderation.ui('avatar')}
disableHoverCard={moderation.blocked}
/>
</View>
<View style={a.flex_1}>
<Text
style={[
t.atoms.text_contrast_medium,
a.text_sm,
web([a.leading_normal, {marginTop: -2}]),
]}
style={[a.text_md, a.font_bold, web(a.leading_normal)]}
numberOfLines={1}>
@{profile.handle}
{convoState.convo?.muted && (
<>
{' '}
&middot;{' '}
<BellStroke size="xs" style={t.atoms.text_contrast_medium} />
</>
)}
{displayName}
</Text>
)}
</View>
</Link>
{!isDeletedAccount && (
<Text
style={[
t.atoms.text_contrast_medium,
a.text_sm,
web([a.leading_normal, {marginTop: -2}]),
]}
numberOfLines={1}>
@{profile.handle}
{convoState.convo?.muted && (
<>
{' '}
&middot;{' '}
<BellStroke
size="xs"
style={t.atoms.text_contrast_medium}
/>
</>
)}
</Text>
)}
</View>
</Link>
{isConvoActive(convoState) && (
<ConvoMenu
convo={convoState.convo}
profile={profile}
currentScreen="conversation"
blockInfo={blockInfo}
{isConvoActive(convoState) && (
<ConvoMenu
convo={convoState.convo}
profile={profile}
currentScreen="conversation"
blockInfo={blockInfo}
/>
)}
</View>
<View
style={[
{
paddingLeft: PFP_SIZE + a.gap_md.gap,
},
]}>
<PostAlerts
modui={moderation.ui('contentList')}
size="large"
style={[a.pt_xs]}
/>
)}
</>
</View>
</View>
)
}
+2 -2
View File
@@ -102,7 +102,7 @@ function SubmitStep({
const t = useTheme()
const [details, setDetails] = useState('')
const control = Dialog.useDialogContext()
const {getAgent} = useAgent()
const agent = useAgent()
const {
mutate: submit,
@@ -124,7 +124,7 @@ function SubmitStep({
reason: details,
} satisfies ComAtprotoModerationCreateReport.InputSchema
await getAgent().createModerationReport(report)
await agent.createModerationReport(report)
}
},
onSuccess: () => {
@@ -0,0 +1,67 @@
import React, {useCallback} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import {logEvent} from 'lib/statsig/statsig'
import {FAB} from '#/view/com/util/fab/FAB'
import * as Toast from '#/view/com/util/Toast'
import {useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {SearchablePeopleList} from './SearchablePeopleList'
export function NewChat({
control,
onNewChat,
}: {
control: Dialog.DialogControlProps
onNewChat: (chatId: string) => void
}) {
const t = useTheme()
const {_} = useLingui()
const {mutate: createChat} = useGetConvoForMembers({
onSuccess: data => {
onNewChat(data.convo.id)
if (!data.convo.lastMessage) {
logEvent('chat:create', {logContext: 'NewChatDialog'})
}
logEvent('chat:open', {logContext: 'NewChatDialog'})
},
onError: error => {
Toast.show(error.message)
},
})
const onCreateChat = useCallback(
(did: string) => {
control.close(() => createChat([did]))
},
[control, createChat],
)
return (
<>
<FAB
testID="newChatFAB"
onPress={control.open}
icon={<Plus size="lg" fill={t.palette.white} />}
accessibilityRole="button"
accessibilityLabel={_(msg`New chat`)}
accessibilityHint=""
/>
<Dialog.Outer
control={control}
testID="newChatDialog"
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
<SearchablePeopleList
title={_(msg`Start a new chat`)}
onSelectChat={onCreateChat}
/>
</Dialog.Outer>
</>
)
}
@@ -16,23 +16,18 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useSession} from '#/state/session'
import {logEvent} from 'lib/statsig/statsig'
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
import {FAB} from '#/view/com/util/fab/FAB'
import * as Toast from '#/view/com/util/Toast'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, native, useTheme, web} from '#/alf'
import {Button} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {TextInput} from '#/components/dms/NewChatDialog/TextInput'
import {TextInput} from '#/components/dms/dialogs/TextInput'
import {canBeMessaged} from '#/components/dms/util'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Text} from '#/components/Typography'
@@ -57,55 +52,228 @@ type Item =
key: string
}
export function NewChat({
control,
onNewChat,
export function SearchablePeopleList({
title,
onSelectChat,
}: {
control: Dialog.DialogControlProps
onNewChat: (chatId: string) => void
title: string
onSelectChat: (did: string) => void
}) {
const t = useTheme()
const {_} = useLingui()
const moderationOpts = useModerationOpts()
const control = Dialog.useDialogContext()
const listRef = useRef<BottomSheetFlatListMethods>(null)
const {currentAccount} = useSession()
const inputRef = useRef<TextInputType>(null)
const {mutate: createChat} = useGetConvoForMembers({
onSuccess: data => {
onNewChat(data.convo.id)
const [searchText, setSearchText] = useState('')
if (!data.convo.lastMessage) {
logEvent('chat:create', {logContext: 'NewChatDialog'})
const {
data: results,
isError,
isFetching,
} = useActorAutocompleteQuery(searchText, true, 12)
const {data: follows} = useProfileFollowsQuery(currentAccount?.did)
const items = useMemo(() => {
let _items: Item[] = []
if (isError) {
_items.push({
type: 'empty',
key: 'empty',
message: _(msg`We're having network issues, try again`),
})
} else if (searchText.length) {
if (results?.length) {
for (const profile of results) {
if (profile.did === currentAccount?.did) continue
_items.push({
type: 'profile',
key: profile.did,
enabled: canBeMessaged(profile),
profile,
})
}
_items = _items.sort(a => {
// @ts-ignore
return a.enabled ? -1 : 1
})
}
logEvent('chat:open', {logContext: 'NewChatDialog'})
},
onError: error => {
Toast.show(error.message)
},
})
} else {
if (follows) {
for (const page of follows.pages) {
for (const profile of page.follows) {
_items.push({
type: 'profile',
key: profile.did,
enabled: canBeMessaged(profile),
profile,
})
}
}
const onCreateChat = useCallback(
(did: string) => {
control.close(() => createChat([did]))
_items = _items.sort(a => {
// @ts-ignore
return a.enabled ? -1 : 1
})
} else {
Array(10)
.fill(0)
.forEach((_, i) => {
_items.push({
type: 'placeholder',
key: i + '',
})
})
}
}
return _items
}, [_, searchText, results, isError, currentAccount?.did, follows])
if (searchText && !isFetching && !items.length && !isError) {
items.push({type: 'empty', key: 'empty', message: _(msg`No results`)})
}
const renderItems = useCallback(
({item}: {item: Item}) => {
switch (item.type) {
case 'profile': {
return (
<ProfileCard
key={item.key}
enabled={item.enabled}
profile={item.profile}
moderationOpts={moderationOpts!}
onPress={onSelectChat}
/>
)
}
case 'placeholder': {
return <ProfileCardSkeleton key={item.key} />
}
case 'empty': {
return <Empty key={item.key} message={item.message} />
}
default:
return null
}
},
[control, createChat],
[moderationOpts, onSelectChat],
)
return (
<>
<FAB
testID="newChatFAB"
onPress={control.open}
icon={<Plus size="lg" fill={t.palette.white} />}
accessibilityRole="button"
accessibilityLabel={_(msg`New chat`)}
accessibilityHint=""
/>
useLayoutEffect(() => {
if (isWeb) {
setImmediate(() => {
inputRef?.current?.focus()
})
}
}, [])
<Dialog.Outer
control={control}
testID="newChatDialog"
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
<SearchablePeopleList onCreateChat={onCreateChat} />
</Dialog.Outer>
</>
const listHeader = useMemo(() => {
return (
<View
style={[
a.relative,
a.pt_md,
a.pb_xs,
a.px_lg,
a.border_b,
t.atoms.border_contrast_low,
t.atoms.bg,
native([a.pt_lg]),
]}>
<View
style={[
a.relative,
native(a.align_center),
a.justify_center,
{height: 32},
]}>
<Button
label={_(msg`Close`)}
size="small"
shape="round"
variant="ghost"
color="secondary"
style={[
a.absolute,
a.z_20,
native({
left: -7,
}),
web({
right: -4,
}),
]}
onPress={() => control.close()}>
{isWeb ? (
<X size="md" fill={t.palette.contrast_500} />
) : (
<ChevronLeft size="md" fill={t.palette.contrast_500} />
)}
</Button>
<Text
style={[
a.z_10,
a.text_lg,
a.font_bold,
a.leading_tight,
t.atoms.text_contrast_high,
]}>
{title}
</Text>
</View>
<View style={[native([a.pt_sm]), web([a.pt_xs])]}>
<SearchInput
inputRef={inputRef}
value={searchText}
onChangeText={text => {
setSearchText(text)
listRef.current?.scrollToOffset({offset: 0, animated: false})
}}
onEscape={control.close}
/>
</View>
</View>
)
}, [
t.atoms.border_contrast_low,
t.atoms.bg,
t.atoms.text_contrast_high,
t.palette.contrast_500,
_,
title,
searchText,
control,
])
return (
<Dialog.InnerFlatList
ref={listRef}
data={items}
renderItem={renderItems}
ListHeaderComponent={listHeader}
stickyHeaderIndices={[0]}
keyExtractor={(item: Item) => item.key}
style={[
web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]),
native({
height: '100%',
paddingHorizontal: 0,
marginTop: 0,
paddingTop: 0,
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
}),
]}
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
keyboardDismissMode="on-drag"
/>
)
}
@@ -293,217 +461,3 @@ function SearchInput({
</View>
)
}
function SearchablePeopleList({
onCreateChat,
}: {
onCreateChat: (did: string) => void
}) {
const t = useTheme()
const {_} = useLingui()
const moderationOpts = useModerationOpts()
const control = Dialog.useDialogContext()
const listRef = useRef<BottomSheetFlatListMethods>(null)
const {currentAccount} = useSession()
const inputRef = useRef<TextInputType>(null)
const [searchText, setSearchText] = useState('')
const {
data: results,
isError,
isFetching,
} = useActorAutocompleteQuery(searchText, true, 12)
const {data: follows} = useProfileFollowsQuery(currentAccount?.did)
const items = useMemo(() => {
let _items: Item[] = []
if (isError) {
_items.push({
type: 'empty',
key: 'empty',
message: _(msg`We're having network issues, try again`),
})
} else if (searchText.length) {
if (results?.length) {
for (const profile of results) {
if (profile.did === currentAccount?.did) continue
_items.push({
type: 'profile',
key: profile.did,
enabled: canBeMessaged(profile),
profile,
})
}
_items = _items.sort(a => {
// @ts-ignore
return a.enabled ? -1 : 1
})
}
} else {
if (follows) {
for (const page of follows.pages) {
for (const profile of page.follows) {
_items.push({
type: 'profile',
key: profile.did,
enabled: canBeMessaged(profile),
profile,
})
}
}
_items = _items.sort(a => {
// @ts-ignore
return a.enabled ? -1 : 1
})
} else {
Array(10)
.fill(0)
.forEach((_, i) => {
_items.push({
type: 'placeholder',
key: i + '',
})
})
}
}
return _items
}, [_, searchText, results, isError, currentAccount?.did, follows])
if (searchText && !isFetching && !items.length && !isError) {
items.push({type: 'empty', key: 'empty', message: _(msg`No results`)})
}
const renderItems = useCallback(
({item}: {item: Item}) => {
switch (item.type) {
case 'profile': {
return (
<ProfileCard
key={item.key}
enabled={item.enabled}
profile={item.profile}
moderationOpts={moderationOpts!}
onPress={onCreateChat}
/>
)
}
case 'placeholder': {
return <ProfileCardSkeleton key={item.key} />
}
case 'empty': {
return <Empty key={item.key} message={item.message} />
}
default:
return null
}
},
[moderationOpts, onCreateChat],
)
useLayoutEffect(() => {
if (isWeb) {
setImmediate(() => {
inputRef?.current?.focus()
})
}
}, [])
const listHeader = useMemo(() => {
return (
<View
style={[
a.relative,
a.pt_md,
a.pb_xs,
a.px_lg,
a.border_b,
t.atoms.border_contrast_low,
t.atoms.bg,
native([a.pt_lg]),
]}>
<View
style={[
a.relative,
native(a.align_center),
a.justify_center,
{height: 32},
]}>
<Button
label={_(msg`Close`)}
size="small"
shape="round"
variant="ghost"
color="secondary"
style={[
a.absolute,
a.z_20,
native({
left: -7,
}),
web({
right: -4,
}),
]}
onPress={() => control.close()}>
{isWeb ? (
<X size="md" fill={t.palette.contrast_500} />
) : (
<ChevronLeft size="md" fill={t.palette.contrast_500} />
)}
</Button>
<Text
style={[
a.z_10,
a.text_lg,
a.font_bold,
a.leading_tight,
t.atoms.text_contrast_high,
]}>
<Trans>Start a new chat</Trans>
</Text>
</View>
<View style={[native([a.pt_sm]), web([a.pt_xs])]}>
<SearchInput
inputRef={inputRef}
value={searchText}
onChangeText={text => {
setSearchText(text)
listRef.current?.scrollToOffset({offset: 0, animated: false})
}}
onEscape={control.close}
/>
</View>
</View>
)
}, [t, _, control, searchText])
return (
<Dialog.InnerFlatList
ref={listRef}
data={items}
renderItem={renderItems}
ListHeaderComponent={listHeader}
stickyHeaderIndices={[0]}
keyExtractor={(item: Item) => item.key}
style={[
web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]),
native({
height: '100%',
paddingHorizontal: 0,
marginTop: 0,
paddingTop: 0,
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
}),
]}
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
keyboardDismissMode="on-drag"
/>
)
}
@@ -0,0 +1,52 @@
import React, {useCallback} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import {logEvent} from 'lib/statsig/statsig'
import * as Toast from '#/view/com/util/Toast'
import * as Dialog from '#/components/Dialog'
import {SearchablePeopleList} from './SearchablePeopleList'
export function SendViaChatDialog({
control,
onSelectChat,
}: {
control: Dialog.DialogControlProps
onSelectChat: (chatId: string) => void
}) {
const {_} = useLingui()
const {mutate: createChat} = useGetConvoForMembers({
onSuccess: data => {
onSelectChat(data.convo.id)
if (!data.convo.lastMessage) {
logEvent('chat:create', {logContext: 'SendViaChatDialog'})
}
logEvent('chat:open', {logContext: 'SendViaChatDialog'})
},
onError: error => {
Toast.show(error.message)
},
})
const onCreateChat = useCallback(
(did: string) => {
control.close(() => createChat([did]))
},
[control, createChat],
)
return (
<Dialog.Outer
control={control}
testID="sendViaChatChatDialog"
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
<SearchablePeopleList
title={_(msg`Send post to...`)}
onSelectChat={onCreateChat}
/>
</Dialog.Outer>
)
}
+3 -3
View File
@@ -7,7 +7,7 @@ export function useRichText(text: string): [RichTextAPI, boolean] {
const [prevText, setPrevText] = React.useState(text)
const [rawRT, setRawRT] = React.useState(() => new RichTextAPI({text}))
const [resolvedRT, setResolvedRT] = React.useState<RichTextAPI | null>(null)
const {getAgent} = useAgent()
const agent = useAgent()
if (text !== prevText) {
setPrevText(text)
setRawRT(new RichTextAPI({text}))
@@ -19,7 +19,7 @@ export function useRichText(text: string): [RichTextAPI, boolean] {
async function resolveRTFacets() {
// new each time
const resolvedRT = new RichTextAPI({text})
await resolvedRT.detectFacets(getAgent())
await resolvedRT.detectFacets(agent)
if (!ignore) {
setResolvedRT(resolvedRT)
}
@@ -28,7 +28,7 @@ export function useRichText(text: string): [RichTextAPI, boolean] {
return () => {
ignore = true
}
}, [text, getAgent])
}, [text, agent])
const isResolving = resolvedRT === null
return [resolvedRT ?? rawRT, isResolving]
}
+16
View File
@@ -3,3 +3,19 @@ import {createSinglePathSVG} from './TEMPLATE'
export const Person_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C3.917 15.521 7.242 12 12 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 19.5 21h-15a1 1 0 0 1-.996-1.094Z',
})
export const PersonCheck_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5a6.69 6.69 0 0 1 2.612.51 1 1 0 0 0 .776-1.844A8.687 8.687 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H11a1 1 0 1 0 0-2H5.679Zm14.835-4.857a1 1 0 0 1 .344 1.371l-3 5a1 1 0 0 1-1.458.286l-2-1.5a1 1 0 0 1 1.2-1.6l1.113.835 2.43-4.05a1 1 0 0 1 1.372-.342Z',
})
export const PersonX_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5 .302 0 .595.018.878.053a1 1 0 0 0 .243-1.985A9.235 9.235 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H12a1 1 0 1 0 0-2H5.679Zm9.614-3.707a1 1 0 0 1 1.414 0L18 16.586l1.293-1.293a1 1 0 0 1 1.414 1.414L19.414 18l1.293 1.293a1 1 0 0 1-1.414 1.414L18 19.414l-1.293 1.293a1 1 0 0 1-1.414-1.414L16.586 18l-1.293-1.293a1 1 0 0 1 0-1.414Z',
})
export const PersonPlus_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.678 19c.71-2.909 3.092-5 6.322-5 .621 0 1.206.077 1.748.218a1 1 0 1 0 .504-1.936A8.931 8.931 0 0 0 12 12c-4.758 0-8.083 3.521-8.496 7.906A1 1 0 0 0 4.5 21H11a1 1 0 1 0 0-2H5.678ZM18 14a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z',
})
export const PersonPlus_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM12 12c-4.758 0-8.083 3.521-8.496 7.906A1 1 0 0 0 4.5 21H15a3 3 0 1 1 0-6c0-.824.332-1.571.87-2.113C14.739 12.32 13.435 12 12 12Zm6 2a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z',
})
-5
View File
@@ -1,5 +0,0 @@
import {createSinglePathSVG} from './TEMPLATE'
export const PersonCheck_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5a6.69 6.69 0 0 1 2.612.51 1 1 0 0 0 .776-1.844A8.687 8.687 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H11a1 1 0 1 0 0-2H5.679Zm14.835-4.857a1 1 0 0 1 .344 1.371l-3 5a1 1 0 0 1-1.458.286l-2-1.5a1 1 0 0 1 1.2-1.6l1.113.835 2.43-4.05a1 1 0 0 1 1.372-.342Z',
})
-5
View File
@@ -1,5 +0,0 @@
import {createSinglePathSVG} from './TEMPLATE'
export const PersonX_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5 .302 0 .595.018.878.053a1 1 0 0 0 .243-1.985A9.235 9.235 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H12a1 1 0 1 0 0-2H5.679Zm9.614-3.707a1 1 0 0 1 1.414 0L18 16.586l1.293-1.293a1 1 0 0 1 1.414 1.414L19.414 18l1.293 1.293a1 1 0 0 1-1.414 1.414L18 19.414l-1.293 1.293a1 1 0 0 1-1.414-1.414L16.586 18l-1.293-1.293a1 1 0 0 1 0-1.414Z',
})
+21
View File
@@ -0,0 +1,21 @@
import {createSinglePathSVG} from './TEMPLATE'
export const OpenQuote_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M7.574 4.178a1 1 0 0 1 .43.822v5h2a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-8c0-2.585 1.162-4.335 2.316-5.417a8.163 8.163 0 0 1 1.569-1.15 7.029 7.029 0 0 1 .738-.36l.016-.005.005-.003h.003v-.001c.001 0 .002 0 .353.936l-.351-.936a1 1 0 0 1 .92.114Zm-1.57 2.588a5.99 5.99 0 0 0-.316.276C4.842 7.835 4.004 9.085 4.004 11v7h5v-6h-2a1 1 0 0 1-1-1V6.766Zm12.57-2.588a1 1 0 0 1 .43.822v5h2a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-8c0-2.585 1.162-4.335 2.316-5.417a8.166 8.166 0 0 1 1.569-1.15 7.038 7.038 0 0 1 .738-.36l.016-.005.005-.003h.003v-.001c.001 0 .002 0 .353.936l-.351-.936a1 1 0 0 1 .92.114Zm-1.57 2.588c-.105.085-.21.177-.316.276-.846.793-1.684 2.043-1.684 3.958v7h5v-6h-2a1 1 0 0 1-1-1V6.766Z',
})
export const OpenQuote_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M8.004 5a1 1 0 0 0-.43-.822c-.57-.395-1.176-.031-1.685.255-.428.24-.998.614-1.569 1.15C3.166 6.665 2.004 8.415 2.004 11v8a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1v-8a1 1 0 0 0-1-1h-2V5ZM19.004 5a1 1 0 0 0-.43-.822c-.57-.395-1.176-.031-1.685.255-.428.24-.998.614-1.569 1.15-1.154 1.082-2.316 2.832-2.316 5.417v8a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1v-8a1 1 0 0 0-1-1h-2V5Z',
})
export const CloseQuote_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M2.004 5a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v8c0 2.585-1.162 4.335-2.316 5.417-.571.536-1.14.91-1.569 1.15a7.01 7.01 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001L6.004 19l.351.936A1 1 0 0 1 5.004 19v-5h-2a1 1 0 0 1-1-1V5Zm5 12.234c.104-.085.21-.177.316-.276.846-.793 1.684-2.043 1.684-3.958V6h-5v6h2a1 1 0 0 1 1 1v4.234Zm6-12.234a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v8c0 2.585-1.162 4.335-2.316 5.417-.571.536-1.14.91-1.569 1.15a7.018 7.018 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001-.352-.936.351.936A1 1 0 0 1 16.004 19v-5h-2a1 1 0 0 1-1-1V5Zm5 12.234V13a1 1 0 0 0-1-1h-2V6h5v7c0 1.915-.838 3.165-1.684 3.958-.106.1-.212.191-.316.276Z',
})
export const CloseQuote_Stroke2_Corner1_Rounded = createSinglePathSVG({
path: 'M2.003 5.999a2 2 0 0 1 2-1.999h5c1.104 0 2 .893 2 1.999V13c0 2.585-1.16 4.335-2.315 5.417-.571.536-1.14.91-1.569 1.15a7.01 7.01 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001L6.004 19l.351.936a1 1 0 0 1-1.351-.935L5 14H4a2 2 0 0 1-2-2.001l.002-6Zm5 11.236L7 12.999A1 1 0 0 0 6 12H4l.003-6h5v7c0 1.915-.837 3.165-1.683 3.958-.106.1-.213.192-.317.277Zm6-11.235a2 2 0 0 1 2-2h5c1.104 0 2 .893 2 1.999V13c0 2.585-1.16 4.335-2.315 5.417-.571.536-1.14.91-1.569 1.15a7.018 7.018 0 0 1-.738.36l-.016.006-.006.002h-.002l-.001.001-.352-.936.351.936A1 1 0 0 1 16.004 19v-5h-1a2 2 0 0 1-2-2V6Zm7 0h-5v6h2a1 1 0 0 1 1 1v4.234c.105-.085.211-.177.317-.276.846-.793 1.684-2.043 1.684-3.958V6Z',
})
export const CloseQuote_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3.004 4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2v5a1 1 0 0 0 .43.822c.57.395 1.176.031 1.685-.255.428-.24.998-.614 1.569-1.15 1.154-1.082 2.316-2.832 2.316-5.417V5a1 1 0 0 0-1-1h-7ZM14.004 4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2v5a1 1 0 0 0 .43.822c.57.395 1.176.031 1.685-.255.428-.24.998-.614 1.569-1.15 1.154-1.082 2.316-2.832 2.316-5.417V5a1 1 0 0 0-1-1h-7Z',
})
+13
View File
@@ -0,0 +1,13 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Repost_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M16.293 2.293a1 1 0 0 1 1.414 0l3 3a1 1 0 0 1 0 1.414l-3 3a1 1 0 0 1-1.414-1.414L17.586 7H5v4a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1h13.586l-1.293-1.293a1 1 0 0 1 0-1.414ZM21 13v5a1 1 0 0 1-1 1H6.414l1.293 1.293a1 1 0 1 1-1.414 1.414l-3-3a1 1 0 0 1 0-1.414l3-3a1 1 0 0 1 1.414 1.414L6.414 17H19v-4a1 1 0 1 1 2 0Z',
})
export const Repost_Stroke2_Corner2_Rounded = createSinglePathSVG({
path: 'M17.957 2.293a1 1 0 1 0-1.414 1.414L17.836 5H6a3 3 0 0 0-3 3v3a1 1 0 1 0 2 0V8a1 1 0 0 1 1-1h11.836l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.47-2.47a1.75 1.75 0 0 0 0-2.474l-2.47-2.47ZM20 12a1 1 0 0 1 1 1v3a3 3 0 0 1-3 3H6.164l1.293 1.293a1 1 0 1 1-1.414 1.414l-2.47-2.47a1.75 1.75 0 0 1 0-2.474l2.47-2.47a1 1 0 0 1 1.414 1.414L6.164 17H18a1 1 0 0 0 1-1v-3a1 1 0 0 1 1-1Z',
})
export const Repost_Stroke2_Corner3_Rounded = createSinglePathSVG({
path: 'M16.793 2.293a1 1 0 0 1 1.414 0L20.5 4.586a2 2 0 0 1 0 2.828l-2.293 2.293a1 1 0 0 1-1.414-1.414L18.086 7H7a2 2 0 0 0-2 2v2a1 1 0 1 1-2 0V9a4 4 0 0 1 4-4h11.086l-1.293-1.293a1 1 0 0 1 0-1.414ZM20 12a1 1 0 0 1 1 1v2a4 4 0 0 1-4 4H5.914l1.293 1.293a1 1 0 1 1-1.414 1.414L3.5 19.414a2 2 0 0 1 0-2.828l2.293-2.293a1 1 0 0 1 1.414 1.414L5.914 17H17a2 2 0 0 0 2-2v-2a1 1 0 0 1 1-1Z',
})
+1 -1
View File
@@ -1,5 +1,5 @@
import {StyleSheet, TextProps} from 'react-native'
import type {SvgProps, PathProps} from 'react-native-svg'
import type {PathProps, SvgProps} from 'react-native-svg'
import {tokens} from '#/alf'
+10 -7
View File
@@ -1,20 +1,19 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {ModerationUI} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {isJustAMute} from '#/lib/moderation'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {atoms as a, useTheme, useBreakpoints, web} from '#/alf'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {Button} from '#/components/Button'
import {Text} from '#/components/Typography'
import {
ModerationDetailsDialog,
useModerationDetailsDialogControl,
} from '#/components/moderation/ModerationDetailsDialog'
import {Text} from '#/components/Typography'
export function ContentHider({
testID,
@@ -52,7 +51,9 @@ export function ContentHider({
<ModerationDetailsDialog control={control} modcause={blur} />
<Button
onPress={() => {
onPress={e => {
e.preventDefault()
e.stopPropagation()
if (!modui.noOverride) {
setOverride(v => !v)
} else {
@@ -121,7 +122,9 @@ export function ContentHider({
{desc.source && blur.type === 'label' && !override && (
<Button
onPress={() => {
onPress={e => {
e.preventDefault()
e.stopPropagation()
control.open()
}}
label={_(
@@ -202,14 +202,14 @@ function AppealForm({
const {gtMobile} = useBreakpoints()
const [details, setDetails] = React.useState('')
const isAccountReport = 'did' in subject
const {getAgent} = useAgent()
const agent = useAgent()
const {mutate, isPending} = useMutation({
mutationFn: async () => {
const $type = !isAccountReport
? 'com.atproto.repo.strongRef'
: 'com.atproto.admin.defs#repoRef'
await getAgent()
await agent
.withProxy('atproto_labeler', label.src)
.createModerationReport({
reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
+44 -13
View File
@@ -1,9 +1,10 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {ModerationCause, ModerationUI} from '@atproto/api'
import {BSKY_LABELER_DID, ModerationCause, ModerationUI} from '@atproto/api'
import {getModerationCauseKey} from '#/lib/moderation'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {
@@ -14,9 +15,11 @@ import {Text} from '#/components/Typography'
export function PostAlerts({
modui,
size,
style,
}: {
modui: ModerationUI
size?: 'medium' | 'large'
includeMute?: boolean
style?: StyleProp<ViewStyle>
}) {
@@ -28,17 +31,31 @@ export function PostAlerts({
<View style={[a.flex_col, a.gap_xs, style]}>
<View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
{modui.alerts.map(cause => (
<PostLabel key={getModerationCauseKey(cause)} cause={cause} />
<PostLabel
key={getModerationCauseKey(cause)}
cause={cause}
size={size}
/>
))}
{modui.informs.map(cause => (
<PostLabel key={getModerationCauseKey(cause)} cause={cause} />
<PostLabel
key={getModerationCauseKey(cause)}
cause={cause}
size={size}
/>
))}
</View>
</View>
)
}
function PostLabel({cause}: {cause: ModerationCause}) {
function PostLabel({
cause,
size,
}: {
cause: ModerationCause
size?: 'medium' | 'large'
}) {
const control = useModerationDetailsDialogControl()
const desc = useModerationCauseDescription(cause)
const t = useTheme()
@@ -47,7 +64,9 @@ function PostLabel({cause}: {cause: ModerationCause}) {
<>
<Button
label={desc.name}
onPress={() => {
onPress={e => {
e.preventDefault()
e.stopPropagation()
control.open()
}}>
{({hovered, pressed}) => (
@@ -55,24 +74,36 @@ function PostLabel({cause}: {cause: ModerationCause}) {
style={[
a.flex_row,
a.align_center,
{paddingLeft: 4, paddingRight: 6, paddingVertical: 1},
a.gap_xs,
a.rounded_sm,
hovered || pressed
? t.atoms.bg_contrast_50
: t.atoms.bg_contrast_25,
? size === 'large'
? t.atoms.bg_contrast_50
: t.atoms.bg_contrast_25
: size === 'large'
? t.atoms.bg_contrast_25
: undefined,
size === 'large'
? {paddingLeft: 4, paddingRight: 6, paddingVertical: 2}
: {paddingRight: 4, paddingVertical: 1},
]}>
<desc.icon size="xs" fill={t.atoms.text_contrast_medium.color} />
{desc.sourceType === 'labeler' &&
desc.sourceDid !== BSKY_LABELER_DID ? (
<UserAvatar
avatar={desc.sourceAvi}
size={size === 'large' ? 16 : 12}
/>
) : (
<desc.icon size="sm" fill={t.atoms.text_contrast_medium.color} />
)}
<Text
style={[
a.text_left,
a.leading_snug,
a.text_xs,
t.atoms.text_contrast_medium,
a.font_semibold,
size === 'large' ? {fontSize: 13} : a.text_xs,
size === 'large' ? t.atoms.text : t.atoms.text_contrast_high,
]}>
{desc.name}
{desc.source ? ` ${desc.source}` : ''}
</Text>
</View>
)}
+4 -1
View File
@@ -23,6 +23,7 @@ interface Props extends ComponentProps<typeof Link> {
iconStyles: StyleProp<ViewStyle>
modui: ModerationUI
profile: AppBskyActorDefs.ProfileViewBasic
interpretFilterAsBlur?: boolean
}
export function PostHider({
@@ -35,6 +36,7 @@ export function PostHider({
iconSize,
iconStyles,
profile,
interpretFilterAsBlur,
...props
}: Props) {
const queryClient = useQueryClient()
@@ -42,7 +44,8 @@ export function PostHider({
const {_} = useLingui()
const [override, setOverride] = React.useState(false)
const control = useModerationDetailsDialogControl()
const blur = modui.blurs[0]
const blur =
modui.blurs[0] || (interpretFilterAsBlur ? modui.filters[0] : undefined)
const desc = useModerationCauseDescription(blur)
const onBeforePress = React.useCallback(() => {
@@ -1,9 +1,14 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {ModerationCause, ModerationDecision} from '@atproto/api'
import {
BSKY_LABELER_DID,
ModerationCause,
ModerationDecision,
} from '@atproto/api'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {getModerationCauseKey} from 'lib/moderation'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {
@@ -62,7 +67,12 @@ function ProfileLabel({cause}: {cause: ModerationCause}) {
? t.atoms.bg_contrast_50
: t.atoms.bg_contrast_25,
]}>
<desc.icon size="sm" fill={t.atoms.text_contrast_medium.color} />
{desc.sourceType === 'labeler' &&
desc.sourceDid !== BSKY_LABELER_DID ? (
<UserAvatar avatar={desc.sourceAvi} size={16} />
) : (
<desc.icon size="sm" fill={t.atoms.text_contrast_medium.color} />
)}
<Text
style={[
a.text_left,
@@ -72,7 +82,6 @@ function ProfileLabel({cause}: {cause: ModerationCause}) {
a.font_semibold,
]}>
{desc.name}
{desc.source ? ` ${desc.source}` : ''}
</Text>
</View>
)}
+6 -6
View File
@@ -7,22 +7,22 @@ import {
import {FeedAPI, FeedAPIResponse} from './types'
export class AuthorFeedAPI implements FeedAPI {
getAgent: () => BskyAgent
agent: BskyAgent
params: GetAuthorFeed.QueryParams
constructor({
getAgent,
agent,
feedParams,
}: {
getAgent: () => BskyAgent
agent: BskyAgent
feedParams: GetAuthorFeed.QueryParams
}) {
this.getAgent = getAgent
this.agent = agent
this.params = feedParams
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.getAgent().getAuthorFeed({
const res = await this.agent.getAuthorFeed({
...this.params,
limit: 1,
})
@@ -36,7 +36,7 @@ export class AuthorFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await this.getAgent().getAuthorFeed({
const res = await this.agent.getAuthorFeed({
...this.params,
cursor,
limit,
+7 -7
View File
@@ -10,27 +10,27 @@ import {FeedAPI, FeedAPIResponse} from './types'
import {createBskyTopicsHeader, isBlueskyOwnedFeed} from './utils'
export class CustomFeedAPI implements FeedAPI {
getAgent: () => BskyAgent
agent: BskyAgent
params: GetCustomFeed.QueryParams
userInterests?: string
constructor({
getAgent,
agent,
feedParams,
userInterests,
}: {
getAgent: () => BskyAgent
agent: BskyAgent
feedParams: GetCustomFeed.QueryParams
userInterests?: string
}) {
this.getAgent = getAgent
this.agent = agent
this.params = feedParams
this.userInterests = userInterests
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const contentLangs = getContentLanguages().join(',')
const res = await this.getAgent().app.bsky.feed.getFeed(
const res = await this.agent.app.bsky.feed.getFeed(
{
...this.params,
limit: 1,
@@ -48,11 +48,11 @@ export class CustomFeedAPI implements FeedAPI {
limit: number
}): Promise<FeedAPIResponse> {
const contentLangs = getContentLanguages().join(',')
const agent = this.getAgent()
const agent = this.agent
const isBlueskyOwned = isBlueskyOwnedFeed(this.params.feed)
const res = agent.session
? await this.getAgent().app.bsky.feed.getFeed(
? await this.agent.app.bsky.feed.getFeed(
{
...this.params,
cursor,
+5 -5
View File
@@ -3,14 +3,14 @@ import {AppBskyFeedDefs, BskyAgent} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
export class FollowingFeedAPI implements FeedAPI {
getAgent: () => BskyAgent
agent: BskyAgent
constructor({getAgent}: {getAgent: () => BskyAgent}) {
this.getAgent = getAgent
constructor({agent}: {agent: BskyAgent}) {
this.agent = agent
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.getAgent().getTimeline({
const res = await this.agent.getTimeline({
limit: 1,
})
return res.data.feed[0]
@@ -23,7 +23,7 @@ export class FollowingFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await this.getAgent().getTimeline({
const res = await this.agent.getTimeline({
cursor,
limit,
})
+8 -8
View File
@@ -27,7 +27,7 @@ export const FALLBACK_MARKER_POST: AppBskyFeedDefs.FeedViewPost = {
}
export class HomeFeedAPI implements FeedAPI {
getAgent: () => BskyAgent
agent: BskyAgent
following: FollowingFeedAPI
discover: CustomFeedAPI
usingDiscover = false
@@ -36,24 +36,24 @@ export class HomeFeedAPI implements FeedAPI {
constructor({
userInterests,
getAgent,
agent,
}: {
userInterests?: string
getAgent: () => BskyAgent
agent: BskyAgent
}) {
this.getAgent = getAgent
this.following = new FollowingFeedAPI({getAgent})
this.agent = agent
this.following = new FollowingFeedAPI({agent})
this.discover = new CustomFeedAPI({
getAgent,
agent,
feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')},
})
this.userInterests = userInterests
}
reset() {
this.following = new FollowingFeedAPI({getAgent: this.getAgent})
this.following = new FollowingFeedAPI({agent: this.agent})
this.discover = new CustomFeedAPI({
getAgent: this.getAgent,
agent: this.agent,
feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')},
userInterests: this.userInterests,
})
+6 -6
View File
@@ -7,22 +7,22 @@ import {
import {FeedAPI, FeedAPIResponse} from './types'
export class LikesFeedAPI implements FeedAPI {
getAgent: () => BskyAgent
agent: BskyAgent
params: GetActorLikes.QueryParams
constructor({
getAgent,
agent,
feedParams,
}: {
getAgent: () => BskyAgent
agent: BskyAgent
feedParams: GetActorLikes.QueryParams
}) {
this.getAgent = getAgent
this.agent = agent
this.params = feedParams
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.getAgent().getActorLikes({
const res = await this.agent.getActorLikes({
...this.params,
limit: 1,
})
@@ -36,7 +36,7 @@ export class LikesFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await this.getAgent().getActorLikes({
const res = await this.agent.getActorLikes({
...this.params,
cursor,
limit,
+6 -6
View File
@@ -7,22 +7,22 @@ import {
import {FeedAPI, FeedAPIResponse} from './types'
export class ListFeedAPI implements FeedAPI {
getAgent: () => BskyAgent
agent: BskyAgent
params: GetListFeed.QueryParams
constructor({
getAgent,
agent,
feedParams,
}: {
getAgent: () => BskyAgent
agent: BskyAgent
feedParams: GetListFeed.QueryParams
}) {
this.getAgent = getAgent
this.agent = agent
this.params = feedParams
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.getAgent().app.bsky.feed.getListFeed({
const res = await this.agent.app.bsky.feed.getListFeed({
...this.params,
limit: 1,
})
@@ -36,7 +36,7 @@ export class ListFeedAPI implements FeedAPI {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await this.getAgent().app.bsky.feed.getListFeed({
const res = await this.agent.app.bsky.feed.getListFeed({
...this.params,
cursor,
limit,
+19 -19
View File
@@ -16,7 +16,7 @@ const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours
export class MergeFeedAPI implements FeedAPI {
userInterests?: string
getAgent: () => BskyAgent
agent: BskyAgent
params: FeedParams
feedTuners: FeedTunerFn[]
following: MergeFeedSource_Following
@@ -26,29 +26,29 @@ export class MergeFeedAPI implements FeedAPI {
sampleCursor = 0
constructor({
getAgent,
agent,
feedParams,
feedTuners,
userInterests,
}: {
getAgent: () => BskyAgent
agent: BskyAgent
feedParams: FeedParams
feedTuners: FeedTunerFn[]
userInterests?: string
}) {
this.getAgent = getAgent
this.agent = agent
this.params = feedParams
this.feedTuners = feedTuners
this.userInterests = userInterests
this.following = new MergeFeedSource_Following({
getAgent: this.getAgent,
agent: this.agent,
feedTuners: this.feedTuners,
})
}
reset() {
this.following = new MergeFeedSource_Following({
getAgent: this.getAgent,
agent: this.agent,
feedTuners: this.feedTuners,
})
this.customFeeds = []
@@ -60,7 +60,7 @@ export class MergeFeedAPI implements FeedAPI {
this.params.mergeFeedSources.map(
feedUri =>
new MergeFeedSource_Custom({
getAgent: this.getAgent,
agent: this.agent,
feedUri,
feedTuners: this.feedTuners,
userInterests: this.userInterests,
@@ -73,7 +73,7 @@ export class MergeFeedAPI implements FeedAPI {
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.getAgent().getTimeline({
const res = await this.agent.getTimeline({
limit: 1,
})
return res.data.feed[0]
@@ -167,7 +167,7 @@ export class MergeFeedAPI implements FeedAPI {
}
class MergeFeedSource {
getAgent: () => BskyAgent
agent: BskyAgent
feedTuners: FeedTunerFn[]
sourceInfo: ReasonFeedSource | undefined
cursor: string | undefined = undefined
@@ -175,13 +175,13 @@ class MergeFeedSource {
hasMore = true
constructor({
getAgent,
agent,
feedTuners,
}: {
getAgent: () => BskyAgent
agent: BskyAgent
feedTuners: FeedTunerFn[]
}) {
this.getAgent = getAgent
this.agent = agent
this.feedTuners = feedTuners
}
@@ -245,7 +245,7 @@ class MergeFeedSource_Following extends MergeFeedSource {
cursor: string | undefined,
limit: number,
): Promise<AppBskyFeedGetTimeline.Response> {
const res = await this.getAgent().getTimeline({cursor, limit})
const res = await this.agent.getTimeline({cursor, limit})
// run the tuner pre-emptively to ensure better mixing
const slices = this.tuner.tune(res.data.feed, {
dryRun: false,
@@ -257,27 +257,27 @@ class MergeFeedSource_Following extends MergeFeedSource {
}
class MergeFeedSource_Custom extends MergeFeedSource {
getAgent: () => BskyAgent
agent: BskyAgent
minDate: Date
feedUri: string
userInterests?: string
constructor({
getAgent,
agent,
feedUri,
feedTuners,
userInterests,
}: {
getAgent: () => BskyAgent
agent: BskyAgent
feedUri: string
feedTuners: FeedTunerFn[]
userInterests?: string
}) {
super({
getAgent,
agent,
feedTuners,
})
this.getAgent = getAgent
this.agent = agent
this.feedUri = feedUri
this.userInterests = userInterests
this.sourceInfo = {
@@ -295,7 +295,7 @@ class MergeFeedSource_Custom extends MergeFeedSource {
try {
const contentLangs = getContentLanguages().join(',')
const isBlueskyOwned = isBlueskyOwnedFeed(this.feedUri)
const res = await this.getAgent().app.bsky.feed.getFeed(
const res = await this.agent.app.bsky.feed.getFeed(
{
cursor,
limit,
+2 -13
View File
@@ -4,7 +4,6 @@ import {
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedThreadgate,
AppBskyRichtextFacet,
BskyAgent,
ComAtprotoLabelDefs,
ComAtprotoRepoUploadBlob,
@@ -15,7 +14,7 @@ import {AtUri} from '@atproto/api'
import {logger} from '#/logger'
import {ThreadgateSetting} from '#/state/queries/threadgate'
import {isNetworkError} from 'lib/strings/errors'
import {shortenLinks} from 'lib/strings/rich-text-manip'
import {shortenLinks, stripInvalidMentions} from 'lib/strings/rich-text-manip'
import {isNative, isWeb} from 'platform/detection'
import {ImageModel} from 'state/models/media/image'
import {LinkMeta} from '../link-meta/link-meta'
@@ -81,17 +80,7 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
opts.onStateChange?.('Processing...')
await rt.detectFacets(agent)
rt = shortenLinks(rt)
// filter out any mention facets that didn't map to a user
rt.facets = rt.facets?.filter(facet => {
const mention = facet.features.find(feature =>
AppBskyRichtextFacet.isMention(feature),
)
if (mention && !mention.did) {
return false
}
return true
})
rt = stripInvalidMentions(rt)
// add quote embed if present
if (opts.quote) {
+2
View File
@@ -1,6 +1,8 @@
import {version} from '../../package.json'
export const BUILD_ENV = process.env.EXPO_PUBLIC_ENV
export const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
export const IS_TESTFLIGHT = false
// This is the commit hash that the current bundle was made from. The user can see the commit hash in the app's settings
// along with the other version info. Useful for debugging/reporting.
-56
View File
@@ -1,56 +0,0 @@
import * as Updates from 'expo-updates'
import {useCallback, useEffect} from 'react'
import {AppState} from 'react-native'
import {logger} from '#/logger'
export function useOTAUpdate() {
// HELPER FUNCTIONS
const checkForUpdate = useCallback(async () => {
logger.debug('useOTAUpdate: Checking for update...')
try {
// Check if new OTA update is available
const update = await Updates.checkForUpdateAsync()
// If updates aren't available stop the function execution
if (!update.isAvailable) {
return
}
// Otherwise fetch the update in the background, so even if the user rejects switching to latest version it will be done automatically on next relaunch.
await Updates.fetchUpdateAsync()
} catch (e) {
logger.error('useOTAUpdate: Error while checking for update', {
message: e,
})
}
}, [])
const updateEventListener = useCallback((event: Updates.UpdateEvent) => {
logger.debug('useOTAUpdate: Listening for update...')
if (event.type === Updates.UpdateEventType.ERROR) {
logger.error('useOTAUpdate: Error while listening for update', {
message: event.message,
})
} else if (event.type === Updates.UpdateEventType.NO_UPDATE_AVAILABLE) {
// Handle no update available
// do nothing
} else if (event.type === Updates.UpdateEventType.UPDATE_AVAILABLE) {
// Handle update available
// open modal, ask for user confirmation, and reload the app
}
}, [])
useEffect(() => {
// ADD EVENT LISTENERS
const updateEventSubscription = Updates.addListener(updateEventListener)
const appStateSubscription = AppState.addEventListener('change', state => {
if (state === 'active' && !__DEV__) {
checkForUpdate()
}
})
// REMOVE EVENT LISTENERS (CLEANUP)
return () => {
updateEventSubscription.remove()
appStateSubscription.remove()
}
}, []) // eslint-disable-line react-hooks/exhaustive-deps
// disable exhaustive deps because we don't want to run this effect again
}
+3 -2
View File
@@ -1,6 +1,7 @@
import {Camera} from 'expo-camera'
import * as MediaLibrary from 'expo-media-library'
import {Linking} from 'react-native'
import {Camera} from 'expo-camera/legacy' // TODO: Migrate to the new one.
import * as MediaLibrary from 'expo-media-library'
import {isWeb} from 'platform/detection'
import {Alert} from 'view/com/util/Alert'
-634
View File
@@ -2,98 +2,6 @@ import React from 'react'
import {StyleProp, TextStyle, ViewStyle} from 'react-native'
import Svg, {Ellipse, Line, Path, Rect} from 'react-native-svg'
export function GridIcon({
style,
solid,
}: {
style?: StyleProp<ViewStyle>
solid?: boolean
}) {
const DIM = 4
const ARC = 2
return (
<Svg width="24" height="24" style={style}>
<Path
d={`M4,1 h${DIM} a${ARC},${ARC} 0 0 1 ${ARC},${ARC} v${DIM} a${ARC},${ARC} 0 0 1 -${ARC},${ARC} h-${DIM} a${ARC},${ARC} 0 0 1 -${ARC},-${ARC} v-${DIM} a${ARC},${ARC} 0 0 1 ${ARC},-${ARC} z`}
strokeWidth={2}
stroke="#000"
fill={solid ? '#000' : undefined}
/>
<Path
d={`M16,1 h${DIM} a${ARC},${ARC} 0 0 1 ${ARC},${ARC} v${DIM} a${ARC},${ARC} 0 0 1 -${ARC},${ARC} h-${DIM} a${ARC},${ARC} 0 0 1 -${ARC},-${ARC} v-${DIM} a${ARC},${ARC} 0 0 1 ${ARC},-${ARC} z`}
strokeWidth={2}
stroke="#000"
fill={solid ? '#000' : undefined}
/>
<Path
d={`M4,13 h${DIM} a${ARC},${ARC} 0 0 1 ${ARC},${ARC} v${DIM} a${ARC},${ARC} 0 0 1 -${ARC},${ARC} h-${DIM} a${ARC},${ARC} 0 0 1 -${ARC},-${ARC} v-${DIM} a${ARC},${ARC} 0 0 1 ${ARC},-${ARC} z`}
strokeWidth={2}
stroke="#000"
fill={solid ? '#000' : undefined}
/>
<Path
d={`M16,13 h${DIM} a${ARC},${ARC} 0 0 1 ${ARC},${ARC} v${DIM} a${ARC},${ARC} 0 0 1 -${ARC},${ARC} h-${DIM} a${ARC},${ARC} 0 0 1 -${ARC},-${ARC} v-${DIM} a${ARC},${ARC} 0 0 1 ${ARC},-${ARC} z`}
strokeWidth={2}
stroke="#000"
fill={solid ? '#000' : undefined}
/>
</Svg>
)
}
export function GridIconSolid({style}: {style?: StyleProp<ViewStyle>}) {
return <GridIcon style={style} solid />
}
export function HomeIcon({
style,
size,
strokeWidth = 4,
}: {
style?: StyleProp<ViewStyle>
size?: string | number
strokeWidth?: number
}) {
return (
<Svg
viewBox="0 0 48 48"
width={size || 24}
height={size || 24}
stroke="currentColor"
fill="none"
style={style}>
<Path
strokeWidth={strokeWidth}
d="M 23.951 2 C 23.631 2.011 23.323 2.124 23.072 2.322 L 8.859 13.52 C 7.055 14.941 6 17.114 6 19.41 L 6 38.5 C 6 39.864 7.136 41 8.5 41 L 18.5 41 C 19.864 41 21 39.864 21 38.5 L 21 28.5 C 21 28.205 21.205 28 21.5 28 L 26.5 28 C 26.795 28 27 28.205 27 28.5 L 27 38.5 C 27 39.864 28.136 41 29.5 41 L 39.5 41 C 40.864 41 42 39.864 42 38.5 L 42 19.41 C 42 17.114 40.945 14.941 39.141 13.52 L 24.928 2.322 C 24.65 2.103 24.304 1.989 23.951 2 Z"
/>
</Svg>
)
}
export function HomeIconSolid({
style,
size,
strokeWidth = 4,
}: {
style?: StyleProp<ViewStyle>
size?: string | number
strokeWidth?: number
}) {
return (
<Svg
viewBox="0 0 48 48"
width={size || 24}
height={size || 24}
stroke="currentColor"
style={style}>
<Path
fill="currentColor"
strokeWidth={strokeWidth}
d="m 23.951,2 c -0.32,0.011 -0.628,0.124 -0.879,0.322 L 8.859,13.52 C 7.055,14.941 6,17.114 6,19.41 V 38.5 C 6,39.864 7.136,41 8.5,41 h 8 c 1.364,0 2.5,-1.136 2.5,-2.5 v -12 C 19,26.205 19.205,26 19.5,26 h 9 c 0.295,0 0.5,0.205 0.5,0.5 v 12 c 0,1.364 1.136,2.5 2.5,2.5 h 8 C 40.864,41 42,39.864 42,38.5 V 19.41 c 0,-2.296 -1.055,-4.469 -2.859,-5.89 L 24.928,2.322 C 24.65,2.103 24.304,1.989 23.951,2 Z"
/>
</Svg>
)
}
// Copyright (c) 2020 Refactoring UI Inc.
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
export function MagnifyingGlassIcon({
@@ -147,90 +55,6 @@ export function MagnifyingGlassIcon2({
)
}
export function MagnifyingGlassIcon2Solid({
style,
size,
strokeWidth = 2,
}: {
style?: StyleProp<ViewStyle>
size?: string | number
strokeWidth?: number
}) {
return (
<Svg
fill="none"
viewBox="0 0 24 24"
strokeWidth={strokeWidth}
stroke="currentColor"
width={size || 24}
height={size || 24}
style={style}>
<Ellipse
cx="12"
cy="10.5"
rx="6.5"
ry="6.5"
stroke="none"
fill="currentColor"
/>
<Ellipse cx="12" cy="10.5" rx="9" ry="9" />
<Line x1="18.5" y1="17" x2="22" y2="20.5" strokeLinecap="round" />
</Svg>
)
}
// https://github.com/Remix-Design/RemixIcon/blob/master/License
export function BellIcon({
style,
size,
strokeWidth = 1.5,
}: {
style?: StyleProp<ViewStyle>
size?: string | number
strokeWidth?: number
}) {
return (
<Svg
fill="none"
viewBox="0 0 24 24"
width={size || 24}
height={size || 24}
strokeWidth={strokeWidth}
stroke="currentColor"
style={style}>
<Path d="M 11.642 2 H 12.442 A 8.6 8.55 0 0 1 21.042 10.55 V 18.1 A 1 1 0 0 1 20.042 19.1 H 4.042 A 1 1 0 0 1 3.042 18.1 V 10.55 A 8.6 8.55 0 0 1 11.642 2 Z" />
<Line x1="9" y1="22" x2="15" y2="22" />
</Svg>
)
}
// https://github.com/Remix-Design/RemixIcon/blob/master/License
export function BellIconSolid({
style,
size,
strokeWidth = 1.5,
}: {
style?: StyleProp<ViewStyle>
size?: string | number
strokeWidth?: number
}) {
return (
<Svg
viewBox="0 0 24 24"
width={size || 24}
height={size || 24}
strokeWidth={strokeWidth}
stroke="currentColor"
style={style}>
<Path
d="M 11.642 2 H 12.442 A 8.6 8.55 0 0 1 21.042 10.55 V 18.1 A 1 1 0 0 1 20.042 19.1 H 4.042 A 1 1 0 0 1 3.042 18.1 V 10.55 A 8.6 8.55 0 0 1 11.642 2 Z"
fill="currentColor"
/>
<Line x1="9" y1="22" x2="15" y2="22" />
</Svg>
)
}
export function CogIcon({
style,
size,
@@ -263,160 +87,6 @@ export function CogIcon({
)
}
export function CogIconSolid({
style,
size,
strokeWidth = 1.5,
}: {
style?: StyleProp<ViewStyle>
size?: string | number
strokeWidth: number
}) {
return (
<Svg
fill="none"
viewBox="0 0 24 24"
width={size || 32}
height={size || 32}
strokeWidth={strokeWidth}
stroke="currentColor"
style={style}>
<Path
strokeLinecap="round"
strokeLinejoin="round"
d="M 9.594 3.94 C 9.684 3.398 10.154 3 10.704 3 L 13.297 3 C 13.847 3 14.317 3.398 14.407 3.94 L 14.62 5.221 C 14.683 5.595 14.933 5.907 15.265 6.091 C 15.339 6.131 15.412 6.174 15.485 6.218 C 15.809 6.414 16.205 6.475 16.56 6.342 L 17.777 5.886 C 18.292 5.692 18.872 5.9 19.147 6.376 L 20.443 8.623 C 20.718 9.099 20.608 9.705 20.183 10.054 L 19.18 10.881 C 18.887 11.121 18.742 11.494 18.749 11.873 C 18.751 11.958 18.751 12.043 18.749 12.128 C 18.742 12.506 18.887 12.878 19.179 13.118 L 20.184 13.946 C 20.608 14.296 20.718 14.9 20.444 15.376 L 19.146 17.623 C 18.871 18.099 18.292 18.307 17.777 18.114 L 16.56 17.658 C 16.205 17.525 15.81 17.586 15.484 17.782 C 15.412 17.826 15.338 17.869 15.264 17.91 C 14.933 18.093 14.683 18.405 14.62 18.779 L 14.407 20.059 C 14.317 20.602 13.847 21 13.297 21 L 10.703 21 C 10.153 21 9.683 20.602 9.593 20.06 L 9.38 18.779 C 9.318 18.405 9.068 18.093 8.736 17.909 C 8.662 17.868 8.589 17.826 8.516 17.782 C 8.191 17.586 7.796 17.525 7.44 17.658 L 6.223 18.114 C 5.708 18.307 5.129 18.1 4.854 17.624 L 3.557 15.377 C 3.282 14.901 3.392 14.295 3.817 13.946 L 4.821 13.119 C 5.113 12.879 5.258 12.506 5.251 12.127 C 5.249 12.042 5.249 11.957 5.251 11.872 C 5.258 11.494 5.113 11.122 4.821 10.882 L 3.817 10.054 C 3.393 9.705 3.283 9.1 3.557 8.624 L 4.854 6.377 C 5.129 5.9 5.709 5.692 6.224 5.886 L 7.44 6.342 C 7.796 6.475 8.191 6.414 8.516 6.218 C 8.588 6.174 8.662 6.131 8.736 6.09 C 9.068 5.907 9.318 5.595 9.38 5.221 Z M 13.5 9.402 C 11.5 8.247 9 9.691 9 12 C 9 13.072 9.572 14.062 10.5 14.598 C 12.5 15.753 15 14.309 15 12 C 15 10.928 14.428 9.938 13.5 9.402 Z"
fill="currentColor"
/>
</Svg>
)
}
// Copyright (c) 2020 Refactoring UI Inc.
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
export function MoonIcon({
style,
size,
strokeWidth = 1.5,
}: {
style?: StyleProp<ViewStyle>
size?: string | number
strokeWidth?: number
}) {
return (
<Svg
fill="none"
viewBox="0 0 24 24"
width={size || 32}
height={size || 32}
strokeWidth={strokeWidth}
stroke="currentColor"
style={style}>
<Path
strokeLinecap="round"
strokeLinejoin="round"
d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"
/>
</Svg>
)
}
// Copyright (c) 2020 Refactoring UI Inc.
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
export function SunIcon({
style,
size,
strokeWidth = 1.5,
}: {
style?: StyleProp<ViewStyle>
size?: string | number
strokeWidth?: number
}) {
return (
<Svg
fill="none"
viewBox="0 0 24 24"
width={size || 32}
height={size || 32}
strokeWidth={strokeWidth}
stroke="currentColor"
style={style}>
<Path
d="M12 3V5.25M18.364 5.63604L16.773 7.22703M21 12H18.75M18.364 18.364L16.773 16.773M12 18.75V21M7.22703 16.773L5.63604 18.364M5.25 12H3M7.22703 7.22703L5.63604 5.63604M15.75 12C15.75 14.0711 14.0711 15.75 12 15.75C9.92893 15.75 8.25 14.0711 8.25 12C8.25 9.92893 9.92893 8.25 12 8.25C14.0711 8.25 15.75 9.92893 15.75 12Z"
strokeLinecap="round"
strokeLinejoin="round"
/>
</Svg>
)
}
// Copyright (c) 2020 Refactoring UI Inc.
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
export function UserIcon({
style,
size,
strokeWidth = 1.5,
}: {
style?: StyleProp<ViewStyle>
size?: string | number
strokeWidth?: number
}) {
return (
<Svg
fill="none"
viewBox="0 0 24 24"
width={size || 32}
height={size || 32}
strokeWidth={strokeWidth}
stroke="currentColor"
style={style}>
<Path
strokeLinecap="round"
strokeLinejoin="round"
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
/>
</Svg>
)
}
export function UserIconSolid({
style,
size,
strokeWidth = 1.5,
}: {
style?: StyleProp<ViewStyle>
size?: string | number
strokeWidth?: number
}) {
return (
<Svg
fill="none"
viewBox="0 0 24 24"
width={size || 32}
height={size || 32}
strokeWidth={strokeWidth}
stroke="currentColor"
style={style}>
<Path
strokeLinecap="round"
strokeLinejoin="round"
fill="currentColor"
d="M 15 9.75 C 15 12.059 12.5 13.503 10.5 12.348 C 9.572 11.812 9 10.822 9 9.75 C 9 7.441 11.5 5.997 13.5 7.152 C 14.428 7.688 15 8.678 15 9.75 Z"
/>
<Path
strokeLinecap="round"
strokeLinejoin="round"
fill="currentColor"
d="M 17.982 18.725 C 16.565 16.849 14.35 15.748 12 15.75 C 9.65 15.748 7.435 16.849 6.018 18.725 M 17.981 18.725 C 16.335 20.193 14.206 21.003 12 21 C 9.794 21.003 7.664 20.193 6.018 18.725"
/>
<Path
strokeLinecap="round"
strokeLinejoin="round"
d="M 17.981 18.725 C 23.158 14.12 21.409 5.639 14.833 3.458 C 8.257 1.277 1.786 7.033 3.185 13.818 C 3.576 15.716 4.57 17.437 6.018 18.725 M 17.981 18.725 C 16.335 20.193 14.206 21.003 12 21 C 9.794 21.003 7.664 20.193 6.018 18.725"
/>
</Svg>
)
}
// Copyright (c) 2020 Refactoring UI Inc.
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
export function UserGroupIcon({
@@ -444,208 +114,6 @@ export function UserGroupIcon({
)
}
export function RepostIcon({
style,
size = 24,
strokeWidth = 1.5,
}: {
style?: StyleProp<TextStyle>
size?: string | number
strokeWidth: number
}) {
return (
<Svg viewBox="0 0 24 24" width={size} height={size} style={style}>
<Path
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinejoin="round"
fill="none"
d="M 14.437 18.081 L 5.475 18.095 C 4.7 18.095 4.072 17.467 4.072 16.692 L 4.082 6.65 L 1.22 10.854 M 4.082 6.65 L 7.006 10.854 M 9.859 6.65 L 18.625 6.654 C 19.4 6.654 20.028 7.282 20.028 8.057 L 20.031 18.081 L 17.167 13.646 M 20.031 18.081 L 22.866 13.646"
/>
</Svg>
)
}
// Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.
export function HeartIcon({
style,
size = 24,
strokeWidth = 1.5,
}: {
style?: StyleProp<TextStyle>
size?: string | number
strokeWidth: number
}) {
return (
<Svg viewBox="0 0 24 24" width={size} height={size} style={style}>
<Path
strokeWidth={strokeWidth}
stroke="currentColor"
fill="none"
d="M 3.859 13.537 L 10.918 20.127 C 11.211 20.4 11.598 20.552 12 20.552 C 12.402 20.552 12.789 20.4 13.082 20.127 L 20.141 13.537 C 21.328 12.431 22 10.88 22 9.259 L 22 9.033 C 22 6.302 20.027 3.974 17.336 3.525 C 15.555 3.228 13.742 3.81 12.469 5.084 L 12 5.552 L 11.531 5.084 C 10.258 3.81 8.445 3.228 6.664 3.525 C 3.973 3.974 2 6.302 2 9.033 L 2 9.259 C 2 10.88 2.672 12.431 3.859 13.537 Z"
/>
</Svg>
)
}
// Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.
export function HeartIconSolid({
style,
size = 24,
}: {
style?: StyleProp<TextStyle>
size?: string | number
}) {
return (
<Svg viewBox="0 0 24 24" width={size} height={size} style={style}>
<Path
fill="currentColor"
stroke="currentColor"
strokeWidth={1}
d="M 3.859 13.537 L 10.918 20.127 C 11.211 20.4 11.598 20.552 12 20.552 C 12.402 20.552 12.789 20.4 13.082 20.127 L 20.141 13.537 C 21.328 12.431 22 10.88 22 9.259 L 22 9.033 C 22 6.302 20.027 3.974 17.336 3.525 C 15.555 3.228 13.742 3.81 12.469 5.084 L 12 5.552 L 11.531 5.084 C 10.258 3.81 8.445 3.228 6.664 3.525 C 3.973 3.974 2 6.302 2 9.033 L 2 9.259 C 2 10.88 2.672 12.431 3.859 13.537 Z"
/>
</Svg>
)
}
export function UpIcon({
style,
size,
strokeWidth = 1.3,
}: {
style?: StyleProp<ViewStyle>
size?: string | number
strokeWidth: number
}) {
return (
<Svg
viewBox="0 0 14 14"
width={size || 24}
height={size || 24}
style={style}>
<Path
strokeWidth={strokeWidth}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
d="M 7 3 L 2 8 L 4.5 8 L 4.5 11.5 L 9.5 11.5 L 9.5 8 L 12 8 L 7 3 Z"
/>
</Svg>
)
}
export function UpIconSolid({
style,
size,
}: {
style?: StyleProp<ViewStyle>
size?: string | number
}) {
return (
<Svg
viewBox="0 0 14 14"
width={size || 24}
height={size || 24}
style={style}>
<Path
strokeWidth={1.3}
stroke="currentColor"
fill="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
d="M 7 3 L 2 8 L 4.5 8 L 4.5 11.5 L 9.5 11.5 L 9.5 8 L 12 8 L 7 3 Z"
/>
</Svg>
)
}
export function DownIcon({
style,
size,
}: {
style?: StyleProp<ViewStyle>
size?: string | number
}) {
return (
<Svg
viewBox="0 0 14 14"
width={size || 24}
height={size || 24}
style={style}>
<Path
strokeWidth={1.3}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
d="M 7 11.5 L 2 6.5 L 4.5 6.5 L 4.5 3 L 9.5 3 L 9.5 6.5 L 12 6.5 L 7 11.5 Z"
/>
</Svg>
)
}
export function DownIconSolid({
style,
size,
}: {
style?: StyleProp<ViewStyle>
size?: string | number
}) {
return (
<Svg
viewBox="0 0 14 14"
width={size || 24}
height={size || 24}
style={style}>
<Path
strokeWidth={1.3}
stroke="currentColor"
fill="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
d="M 7 11.5 L 2 6.5 L 4.5 6.5 L 4.5 3 L 9.5 3 L 9.5 6.5 L 12 6.5 L 7 11.5 Z"
/>
</Svg>
)
}
// Copyright (c) 2020 Refactoring UI Inc.
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
export function CommentBottomArrow({
style,
size,
strokeWidth = 1.3,
}: {
style?: StyleProp<TextStyle>
size?: string | number
strokeWidth?: number
}) {
let color = 'currentColor'
if (
style &&
typeof style === 'object' &&
'color' in style &&
typeof style.color === 'string'
) {
color = style.color
}
return (
<Svg
fill="none"
viewBox="0 0 24 24"
strokeWidth={strokeWidth || 2.5}
stroke={color}
width={size || 24}
height={size || 24}
style={style}>
<Path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.068.157 2.148.279 3.238.364.466.037.893.281 1.153.671L12 21l2.652-3.978c.26-.39.687-.634 1.153-.67 1.09-.086 2.17-.208 3.238-.365 1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
/>
</Svg>
)
}
export function SquareIcon({
style,
size,
@@ -715,33 +183,6 @@ export function RectTallIcon({
)
}
export function ComposeIcon({
style,
size,
strokeWidth = 1.5,
}: {
style?: StyleProp<TextStyle>
size?: string | number
strokeWidth?: number
}) {
return (
<Svg
fill="none"
viewBox="0 0 24 24"
strokeWidth={strokeWidth}
stroke="currentColor"
width={size || 24}
height={size || 24}
style={style}>
<Path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
/>
</Svg>
)
}
export function ComposeIcon2({
style,
size,
@@ -783,54 +224,6 @@ export function ComposeIcon2({
)
}
export function SquarePlusIcon({
style,
size,
strokeWidth = 1.5,
}: {
style?: StyleProp<TextStyle>
size?: string | number
strokeWidth?: number
}) {
return (
<Svg
viewBox="0 0 24 24"
strokeWidth={strokeWidth}
stroke="currentColor"
width={size || 24}
height={size || 24}
style={style}>
<Line
strokeLinecap="round"
strokeLinejoin="round"
x1="12"
y1="5.5"
x2="12"
y2="18.5"
strokeWidth={strokeWidth * 1.5}
/>
<Line
strokeLinecap="round"
strokeLinejoin="round"
x1="5.5"
y1="12"
x2="18.5"
y2="12"
strokeWidth={strokeWidth * 1.5}
/>
<Rect
strokeWidth={strokeWidth}
x="4"
y="4"
width="16"
height="16"
rx="4"
ry="4"
/>
</Svg>
)
}
export function InfoCircleIcon({
style,
size,
@@ -947,30 +340,3 @@ export function ShieldExclamation({
</Svg>
)
}
export function ListIcon({
style,
size,
strokeWidth = 1.5,
}: {
style?: StyleProp<TextStyle>
size?: string | number
strokeWidth?: number
}) {
return (
<Svg
fill="none"
viewBox="0 0 24 24"
strokeWidth={strokeWidth || 1.5}
stroke="currentColor"
width={size}
height={size}
style={style}>
<Path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
/>
</Svg>
)
}
+4
View File
@@ -159,3 +159,7 @@ async function downloadUrl(href: string, filename: string) {
a.download = filename
a.click()
}
export async function safeDeleteAsync() {
// no-op
}
@@ -6,15 +6,15 @@ import {
} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {getDefinition, getLabelStrings} from './useLabelInfo'
import {useLabelDefinitions} from '#/state/preferences'
import {useGlobalLabelStrings} from './useGlobalLabelStrings'
import {Props as SVGIconProps} from '#/components/icons/common'
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {useLabelDefinitions} from '#/state/preferences'
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {Props as SVGIconProps} from '#/components/icons/common'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
import {useGlobalLabelStrings} from './useGlobalLabelStrings'
import {getDefinition, getLabelStrings} from './useLabelInfo'
export interface ModerationCauseDescription {
icon: React.ComponentType<SVGIconProps>
@@ -22,6 +22,8 @@ export interface ModerationCauseDescription {
description: string
source?: string
sourceType?: ModerationCauseSource['type']
sourceAvi?: string
sourceDid?: string
}
export function useModerationCauseDescription(
@@ -138,6 +140,8 @@ export function useModerationCauseDescription(
description: strings.description,
source,
sourceType: cause.source.type,
sourceAvi: labeler?.creator.avatar,
sourceDid: cause.label.src,
}
}
// should never happen
+36 -41
View File
@@ -14,12 +14,12 @@ const SERVICE_DID = (serviceUrl?: string) =>
: 'did:web:api.bsky.app'
async function registerPushToken(
getAgent: () => BskyAgent,
agent: BskyAgent,
account: SessionAccount,
token: Notifications.DevicePushToken,
) {
try {
await getAgent().api.app.bsky.notification.registerPush({
await agent.api.app.bsky.notification.registerPush({
serviceDid: SERVICE_DID(account.service),
platform: devicePlatform,
token: token.data,
@@ -47,7 +47,7 @@ async function getPushToken(skipPermissionCheck = false) {
}
export function useNotificationsRegistration() {
const {getAgent} = useAgent()
const agent = useAgent()
const {currentAccount} = useSession()
React.useEffect(() => {
@@ -60,57 +60,52 @@ export function useNotificationsRegistration() {
// According to the Expo docs, there is a chance that the token will change while the app is open in some rare
// cases. This will fire `registerPushToken` whenever that happens.
const subscription = Notifications.addPushTokenListener(async newToken => {
registerPushToken(getAgent, currentAccount, newToken)
registerPushToken(agent, currentAccount, newToken)
})
return () => {
subscription.remove()
}
}, [currentAccount, getAgent])
}, [currentAccount, agent])
}
export function useRequestNotificationsPermission() {
const gate = useGate()
const {currentAccount} = useSession()
return React.useCallback(
async (context: 'StartOnboarding' | 'AfterOnboarding' | 'Login') => {
const permissions = await Notifications.getPermissionsAsync()
return async (context: 'StartOnboarding' | 'AfterOnboarding' | 'Login') => {
const permissions = await Notifications.getPermissionsAsync()
if (
!currentAccount ||
!isNative ||
permissions?.status === 'granted' ||
(permissions?.status === 'denied' && !permissions?.canAskAgain)
) {
return
}
if (
context === 'StartOnboarding' &&
gate('request_notifications_permission_after_onboarding')
) {
return
}
if (
context === 'AfterOnboarding' &&
!gate('request_notifications_permission_after_onboarding')
) {
return
}
if (
!isNative ||
permissions?.status === 'granted' ||
permissions?.status === 'denied'
) {
return
}
if (
context === 'StartOnboarding' &&
gate('request_notifications_permission_after_onboarding_v2')
) {
return
}
if (
context === 'AfterOnboarding' &&
!gate('request_notifications_permission_after_onboarding_v2')
) {
return
}
const res = await Notifications.requestPermissionsAsync()
logEvent('notifications:request', {
context: context,
status: res.status,
})
const res = await Notifications.requestPermissionsAsync()
logEvent('notifications:request', {
context: context,
status: res.status,
})
if (res.granted) {
// This will fire a pushTokenEvent, which will handle registration of the token
getPushToken(true)
}
},
[gate, currentAccount],
)
if (res.granted) {
// This will fire a pushTokenEvent, which will handle registration of the token
getPushToken(true)
}
}
}
export async function decrementBadgeCount(by: number | 'reset' = 1) {
+7 -2
View File
@@ -1,5 +1,6 @@
import {NavigationProp} from '@react-navigation/native'
import {State, RouteParams} from './types'
import {RouteParams, State} from './types'
export function getRootNavigation<T extends {}>(
nav: NavigationProp<T>,
@@ -10,7 +11,11 @@ export function getRootNavigation<T extends {}>(
return nav
}
export function getCurrentRoute(state: State) {
export function getCurrentRoute(state?: State) {
if (!state) {
return {name: 'Home'}
}
let node = state.routes[state.index || 0]
while (node.state?.routes && typeof node.state?.index === 'number') {
node = node.state?.routes[node.state?.index]
+1 -1
View File
@@ -38,7 +38,7 @@ export type CommonNavigatorParams = {
AccessibilitySettings: undefined
Search: {q?: string}
Hashtag: {tag: string; author?: string}
MessagesConversation: {conversation: string}
MessagesConversation: {conversation: string; embed?: string}
MessagesSettings: undefined
}

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