Compare commits

..

57 Commits

Author SHA1 Message Date
Eric Bailey fc639f0cdc Button docs 2024-04-04 15:27:58 -05:00
Hailey c190fd58ec replace deprecated term with q in search actor (#3405) 2024-04-04 11:13:11 -07:00
dan b783745b2e Extract query key roots to constants (#3404)
* Extract query key roots to constants

* Dedupe labelers-detailed-info

* Align naming
2024-04-04 18:57:38 +01:00
Paul Frazee b26abf212e Enable tags on profile descriptions (close #3081) (#3397) 2024-04-04 10:52:35 -07:00
surfdude29 8b3017bca0 Alphabetise languages by language code (#3350)
* Alphabetise languages in lingui.config.js

* Alphabetise languages in helpers.ts

* Alphabetise languages in i18n.ts

* Alphabetise languages in i18n.ts

* Alphabetise languages in i18n.web.ts

* Alphabetise languages in languages.ts
2024-04-04 10:51:54 -07:00
Jan-Olof Eriksson 51e939ed3d Bug fixes and some improvements to Finnish localization (#3306)
* Update messages.po

Couple of nasty bugs.

* Update messages.po

Bugs and improvements

* Update messages.po

Hashtag = aihetunniste, not just "tunniste"

* Update messages.po

typo

* Update messages.po

Liked by, tykännyt

* Update messages.po

More translations.

* Update messages.po

* Update messages.po

* Update messages.po

Following - Seurataan

* Update messages.po

one typo

* Update messages.po

line 352 fixed
2024-04-04 10:49:04 -07:00
Samuel Newman 9f657fbace Open share sheet when long pressing link (#3317)
* uitextview use library w/ fixes

bump

bump

multiple uitextview fixes

* bump

* Open share sheet on link long press

* rm package manager field

* add link warning to longpress

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-04-04 10:37:57 -07:00
Hailey 4d28dcc48f rm 'pornography' label wording (#3402)
* rm 'pornography'

* add `adult content` to `REASONSEXUAL`
2024-04-04 10:15:04 -07:00
dan 3e88fdfbc4 Don't mutate array (#3403) 2024-04-04 18:14:44 +01:00
dan 19b1721ef3 Fix profile follow from Likes (#3401) 2024-04-04 18:03:58 +01:00
dan 4cc57f4bfd Lint against strings without wrapping <Text> (#3398)
* Add a rudimentary rule

* Get the rule passing

* Support special-casing text props

* More tests
2024-04-04 17:32:50 +01:00
Hailey 8e393b16f5 Simplify list logic further to prevent misuse (#3334)
* simplify list logic further

more simplification

simplify by removing `isEmpty`

use `isFetchingNextPage` everywhere for clarity

change `isFetching` to `isFetchingNextPage` for clarity

remove some useless `useMemo`s

move `renderItem` and `keyExtractor` out of component

* clean bundle size check

* update deploy

* adjust

* adjust

* one test

* try now

* test it

* done
2024-04-03 20:59:33 -07:00
Hailey b1bd7ab6e3 Add webpack analyzer, PR comments for webpack bundle size (#3383)
try that again 9

try that again 8

try that again 7

try that again 6

try that again 5

try that again 4

try that again 3

try that again 2

try that again

merge base 3

merge base 2

merge base

use latest main commit rather than tag

webpack diff analyzer

use cache v4

use cache

again...

blegh

blegh

try cache again

cache

try again

one more adjutment

adjust

okay again maybe

maybe now?

maybe now

oops again

oops

add diff comments

add open analyzer script

add webpack analyzer
2024-04-03 19:31:29 -07:00
Gildásio Filho 9d9c9095ee Update pt-BR localization to latest version (#3371)
* Update messages.po

* Update messages.po

* Update src/locale/locales/pt-BR/messages.po

Co-authored-by: Gleydson Rodrigues <gleydsonsr@gmail.com>

* Update src/locale/locales/pt-BR/messages.po

Co-authored-by: Gleydson Rodrigues <gleydsonsr@gmail.com>

* Update src/locale/locales/pt-BR/messages.po

Co-authored-by: Gleydson Rodrigues <gleydsonsr@gmail.com>

* Update src/locale/locales/pt-BR/messages.po

Co-authored-by: Gleydson Rodrigues <gleydsonsr@gmail.com>

* Update src/locale/locales/pt-BR/messages.po

Co-authored-by: Gleydson Rodrigues <gleydsonsr@gmail.com>

* Update messages.po

---------

Co-authored-by: Gleydson Rodrigues <gleydsonsr@gmail.com>
2024-04-03 19:22:10 -07:00
Samuel Newman 712768dd8f Use ALF for the account quick switch dialog (#3327)
* Use ALF for account quick switch

* clean up modal type

* add haptics to dialog opening

* move account list to it's own component and share

* make tick slightly darker
2024-04-03 19:18:14 -07:00
Ivan Beà 8cdd8394df Update catalan localization messages.po (#3307)
* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

Apply corrections suggested by @surfdude29, thanks

* Update messages.po

adding @jordimas corrections
2024-04-03 18:53:58 -07:00
dan e51ccb46b8 Scope query client per DID (#3333)
* Move QueryProvider inside the key

* Pull useQueryClient-dependent code down in App.native

* Remove useQueryClient dependency from session provider

* Scope query client per DID
2024-04-04 02:51:10 +01:00
Hima db3cd3e821 Fix & Add: Japanese Translation (6) (#3304)
* Create test.txt

* Delete src/locale/locales/ja/messages.po

* Add files via upload

* Delete src/locale/locales/ja/test.txt

* messages.po を更新

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* messages.po を更新

* messages.po を更新

* messages.po を更新

* messages.po を更新
2024-04-03 18:49:48 -07:00
Minseo Lee af9b961411 Fix magnify icon (#3292)
* Update icons.tsx

* Update icons.tsx
2024-04-03 18:49:14 -07:00
Minseo Lee 7cf22e2047 Update Korean localization (#3281)
* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po
2024-04-03 18:47:04 -07:00
Hailey 7fb117d213 Upgrade UITextView to latest (#3090)
* uitextview use library w/ fixes

bump

bump

multiple uitextview fixes

* bump

* update to latest

* cleanup
2024-04-03 18:05:03 -07:00
Hailey a356b1be1a Work around GitHub false-positive for passing secret to another action (#3396)
* run bundle/deploy on main pushes

* don't fetch from origin if not necessary

* fix false-postive for secret in outputs

rm test

test 'false'

rm test

use in both places

workaround "secret" string

add a manual testflight deployment option for android

add a manual testflight deployment option for android

add a manual testflight deployment option for android

* rm test code
2024-04-03 18:04:48 -07:00
Kevin Scannell cad0590694 Irish PO file thanks to @kingdonncha1.bsky.social (#2924)
* Irish PO file thanks to @kingdonncha1.bsky.social

* feat: add Gaeilge options added by @kscanne into the GUI

---------

Co-authored-by: Kat Gaea <kat.knight@newgaea.net>
2024-04-03 17:32:53 -07:00
Paul Frazee 93fbd91193 Add cursor to 'learn more' on screenhider (close #2871) (#3395) 2024-04-03 17:28:31 -07:00
Paul Frazee 9d5c82d9a8 Merge branch 'atiksoftware-main' into main 2024-04-03 17:15:33 -07:00
Paul Frazee 55e4ea84f8 Add turkish to app languages 2024-04-03 16:50:15 -07:00
dan a32eb96210 Show redbox in DEV on string <View> child for web (#3394) 2024-04-04 00:46:49 +01:00
Paul Frazee a4d4562aba Merge branch 'main' of https://github.com/atiksoftware/social-app into atiksoftware-main 2024-04-03 16:31:13 -07:00
Hailey eda98679df don't fetch from origin if unnecessary when performing OTA deploy (#3392)
* run bundle/deploy on main pushes

* don't fetch from origin if not necessary
2024-04-03 16:27:17 -07:00
Hailey 8e882a4579 run bundle/deploy on main pushes (#3391) 2024-04-03 16:00:52 -07:00
dan 295a0949f4 Factor out QueryProvider (#3331) 2024-04-03 23:53:44 +01:00
dan 6c728f79de Make notifications init reactive to queryClient (#3329) 2024-04-03 23:42:28 +01:00
dan fc1e30afd6 Thread queryClient explicitly through (#3328)
* Pass queryClient explicitly to resetProfilePostsQueries

* Pass queryClient explicitly to updatePostShadow

* Pass queryClient explicitly to updateProfileShadow
2024-04-03 23:33:46 +01:00
Hailey 73df7e53b3 Add OTA updates support for testflight channel (#3291)
* some progress

another adjustment, testing

another adjustment, testing

fix again

fix again

set default runtime version

fix

test this script

test this script

test this script

add build numbers to the deployment url

clean

give script access to build number

add `useBuildNumberEnv` without a bump

new line

fix missing async

add channel name to deployment url

add updates check on launch for testflight users

ver bump

init updates on launch for native

add `testflight` as default in build submit

add is_testflight check

* disable inline predictions to prevent ios composer jank

* temp bump

* Revert "temp bump"

This reverts commit 44c51134a3.

* adjustments

version bump

adjust

fixes

test

* cleanup and finalize

drop check down to every 15 minutes

adjustments

change to 15 mins

use jq to get version if necessary

rm test on push

figured it out

remove nightly testflight releases

test again again again again again AGAIN ONCE MORE

test again again again again again AGAIN

test again again again again again AGAIN

test again again again again again

test again again again again

test again again again

test again again

test again

test

test

test

run deploy if necessary

run deploy if necessary

run deploy if necessary

run deploy if necessary

run deploy if necessary

remove test message

fix environment

oops

cleanup

merge in changes

* remove unnecessary `workflow_call`

* remove changes that have been merged into main now

* finalize android

update git ignore

rm test stuff from the bundle action

remove test message

test message

fix

test message

test message

few android fixes

few android fixes

fix jq

add a test message

fix slack webhook

create android deployments test 2

create android deployments

add `testflight-android` profile to eas.json

more cleanup

some more cleanup

simplify some logic

remove unnecessary channel

rename to `useOTAUpdates`

* rm test portion
2024-04-03 15:14:44 -07:00
Danni 02b2ab4f1f Update Indonesian ID translation to latest (#2803)
* Update Indonesian translation.

* Apply suggestions from code review

Co-authored-by: Fafin <151415765+kodebanget@users.noreply.github.com>
Co-authored-by: Aditya Dirgantara <ccoremapd@icloud.com>

* Update src/locale/locales/id/messages.po

Co-authored-by: Aditya Dirgantara <ccoremapd@icloud.com>

* Change Umpan word to Feed on ID locale

---------

Co-authored-by: Fafin <151415765+kodebanget@users.noreply.github.com>
Co-authored-by: Aditya Dirgantara <ccoremapd@icloud.com>
2024-04-03 14:30:53 -07:00
Frudrax Cheng b76bf9484d Add zh-TW localization (#2781)
* add zh-TW localization

* fix typo

* fix languages.ts

* improve translation

* improve translation

* 部分翻譯修正
- 使用全形標點符號
- 將部分簡體字修正為正體字
- 更改部分用語

* fix incorrectly commented strings

* small fix and suggestion

* update translation team

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* modify the translation based on the review suggestions

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: noeFly <jeffyang0123@gmail.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: noeFly <jeffyang0123@gmail.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: noeFly <jeffyang0123@gmail.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>

* modify translation by review

* modify translation by review

* modify translation by review

* modify translation by review

* modify translation by review

* modify translation by review

* minor fixes

* minor fixes

* Update messages.po

* translate new strings

* fix typo

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Howard Guo <50100922+toto6038@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Howard Guo <50100922+toto6038@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Howard Guo <50100922+toto6038@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Howard Guo <50100922+toto6038@users.noreply.github.com>

* Update src/locale/locales/zh-TW/messages.po

Co-authored-by: Howard Guo <50100922+toto6038@users.noreply.github.com>

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* fixed typo

* Update messages.po

* correct translation

* Update messages.po

* modify translation by review

* Update messages.po

* Update messages.po

---------

Co-authored-by: noeFly <jeffyang0123@gmail.com>
Co-authored-by: Kuwa Lee <kuwalee1069@gmail.com>
Co-authored-by: Yuki Cheung <13027432+snowleo208@users.noreply.github.com>
Co-authored-by: Howard Guo <50100922+toto6038@users.noreply.github.com>
2024-04-03 14:30:04 -07:00
dan 75d6535402 Remove Button error boundary (#3372) 2024-04-03 19:11:29 +01:00
Samuel Newman 75c9da931c Add share warning to big share button in post ctrl (#3367) 2024-04-03 19:03:54 +01:00
Samuel Newman 907c7c0586 make entire recent search clickable (#3387) 2024-04-03 16:13:29 +01:00
dan 8de27160f5 Remove unnecessary yarn call (#3380) 2024-04-03 03:05:53 +01:00
dan 67fca733fa Use Prettier at the root (#3375) 2024-04-02 12:02:20 +01:00
dan a45da17c48 Separate ESLint and Prettier (#3373)
* Disable legacy Prettier ESLint integration

* Run Prettier separately

* Add caching

* Separate -some and -all

* Reduce node inits
2024-04-01 22:58:31 -07:00
Hailey 0ff7e71ee3 disable inline predictions to prevent ios composer jank (#3363)
* disable inline predictions to prevent ios composer jank

* temp bump

* Revert "temp bump"

This reverts commit 44c51134a3.
2024-03-31 15:55:30 -07:00
Samuel Newman 3f79cb3e12 apply bg color to entire scrollview (#3364) 2024-03-31 14:51:31 -07:00
surfdude29 69217f9a09 Mark Retry and Go Back buttons in Error.tsx for localisation (#3352)
* Mark `Retry` and `Go Back` buttons in Error.tsx for localisation

* add and sort imports

* sort imports better
2024-03-29 17:51:35 -07:00
Hailey ef24611a1f revert change for closing composer on web only (#3343) 2024-03-22 17:05:39 -07:00
Paul Frazee 0baf9f9919 1.75 2024-03-22 13:07:54 -07:00
surfdude29 d507fa9f38 Mark under13 and Next and Back buttons in signup for localisation (#3341)
* Mark under13 string for translation

* Mark buttons for localisation

* tweak label

* wrap in `ButtonText`

* lint

* try to fix lint again
2024-03-22 10:18:00 -07:00
Eric Bailey 5b44aa962f Wrap Button children in an error boundary (#3340)
* Wrap Button children in an error boundary

* Check for Trans component
2024-03-22 12:14:47 -05:00
Eric Bailey f04932140a Fix crash during onboarding (#3338)
* Fix crash during onboarding

* Fix other usage
2024-03-22 11:32:49 -05:00
Samuel Newman b27a0b8c97 Merge pull request #3337 from bluesky-social/samuel/scrollable-loggedoutlayout
Move scrollview to `LoggedOutLayout` to fix scrolling on web
2024-03-22 15:36:10 +00:00
Hailey 772d528145 fix android back after closing composer (#3330) 2024-03-22 08:16:34 -07:00
Samuel Newman 4ec434926e move auth flows scrollview to loggedin layout 2024-03-22 13:49:19 +00:00
Samuel Newman 6f2dfcb502 Merge pull request #3326 from bluesky-social/samuel/lowercase-i
Change Sign In -> Sign in on Splashscreen
2024-03-22 00:34:42 +00:00
Samuel Newman a3eab31bda Sign In -> Sign in 2024-03-21 22:24:37 +00:00
dan df7f9856bb Use logger.error instead of rethrow (#3323) 2024-03-21 14:39:19 -07:00
Atiksoftware Corporation 310e5ea35b Create messages.po 2024-02-13 15:02:20 +03:00
138 changed files with 20041 additions and 4285 deletions
+35
View File
@@ -1,3 +1,5 @@
const bskyEslint = require('./eslint')
module.exports = {
root: true,
extends: [
@@ -13,10 +15,43 @@ module.exports = {
'react',
'lingui',
'simple-import-sort',
'bsky-internal',
],
rules: {
// Temporary until https://github.com/facebook/react-native/pull/43756 gets into a release.
'prettier/prettier': 0,
'react/no-unescaped-entities': 0,
'react-native/no-inline-styles': 0,
'bsky-internal/avoid-unwrapped-text': [
'error',
{
impliedTextComponents: [
'Button', // TODO: Not always safe.
'ButtonText',
'DateField.Label',
'Description',
'H1',
'H2',
'H3',
'H4',
'H5',
'H6',
'InlineLink',
'Label',
'P',
'Prompt.Title',
'Prompt.Description',
'Prompt.Cancel', // TODO: Not always safe.
'Prompt.Action', // TODO: Not always safe.
'TextField.Label',
'TextField.Suffix',
'Title',
'Toggle.Label',
'ToggleButton.Button', // TODO: Not always safe.
],
impliedTextProps: ['FormContainer title'],
},
],
'simple-import-sort/imports': [
'warn',
{
+36 -1
View File
@@ -8,6 +8,7 @@ on:
type: choice
description: Build profile to use
options:
- testflight-android
- production
jobs:
@@ -59,7 +60,41 @@ jobs:
echo "$json" > google-services.json
- name: 🏗️ EAS Build
run: yarn use-build-number eas build -p android --profile production --local --output build.aab --non-interactive
run: yarn use-build-number-with-bump eas build -p android --profile ${{ inputs.profile || 'testflight-android' }} --local --output build.aab --non-interactive
- name: 🚀 Deploy
if: ${{ inputs.profile == 'production' }}
run: eas submit -p android --non-interactive --path build.aab
- name: ✍️ Rename bundle
if: ${{ inputs.profile != 'production' }}
run: mv build.aab build.apk
- name: ⏰ Get a timestamp
id: timestamp
if: ${{ inputs.profile != 'production' }}
uses: nanzm/get-time-action@master
with:
format: 'MM-DD-HH-mm-ss'
- name: 🚀 Upload Artifact
id: upload-artifact
if: ${{ inputs.profile != 'production' }}
uses: actions/upload-artifact@v4
with:
retention-days: 30
compression-level: 0
name: build-${{ steps.timestamp.outputs.time }}.apk
path: build.apk
- name: 🔔 Notify Slack
if: ${{ inputs.profile != 'production' }}
uses: slackapi/slack-github-action@v1.25.0
with:
payload: |
{
"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact.outputs.artifact-url }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
+2 -3
View File
@@ -2,14 +2,13 @@
name: Build and Submit iOS
on:
schedule:
- cron: '0 5 * * *'
workflow_dispatch:
inputs:
profile:
type: choice
description: Build profile to use
options:
- testflight
- production
jobs:
@@ -69,7 +68,7 @@ jobs:
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
- name: 🏗️ EAS Build
run: yarn use-build-number eas build -p ios --profile production --local --output build.ipa --non-interactive
run: yarn use-build-number-with-bump eas build -p ios --profile ${{ inputs.profile || 'testflight' }} --local --output build.ipa --non-interactive
- name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path build.ipa
+239 -9
View File
@@ -2,8 +2,17 @@
name: Bundle and Deploy EAS Update
on:
push:
branches:
- main
workflow_dispatch:
inputs:
channel:
type: choice
description: Deployment channel to use
options:
- testflight
- production
runtimeVersion:
type: string
description: Runtime version (in x.x.x format) that this update is for
@@ -13,13 +22,50 @@ jobs:
bundleDeploy:
name: Bundle and Deploy EAS Update
runs-on: ubuntu-latest
outputs:
fingerprint-is-different: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different }}
steps:
- name: Check for EXPO_TOKEN
run: >
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
# Validate the version if one is supplied. This should generally happen if the update is for a production client
- name: 🧐 Validate version
if: ${{ inputs.runtimeVersion }}
run: |
[[ "${{ github.event.inputs.runtimeVersion }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1
if [ -z "${{ inputs.runtimeVersion }}" ]; then
[[ "${{ inputs.runtimeVersion }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 100
- 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 [ -z "${{ inputs.channel == 'production' }}" ]; then
echo base-commit=$(git show-ref -s ${{ inputs.runtimeVersion }}) >> "$GITHUB_OUTPUT"
else
echo base-commit=$(git log -n 1 --skip 1 main --pretty=format:'%H') >> "$GITHUB_OUTPUT"
fi
- name: ✓ Make sure we found a base commit
run: |
if [ -z "${{ steps.base-commit.outputs.base-commit }}" ]; then
echo "Could not find a base commit for this release. Exiting."
exit 1
fi
- name: 🔧 Setup Node
uses: actions/setup-node@v4
@@ -30,13 +76,177 @@ jobs:
- name: ⚙️ Install Dependencies
run: yarn install
- name: 🪛 Install jq
uses: dcarbone/install-jq-action@v2
# Run the fingerprint
- name: 📷 Check fingerprint
id: fingerprint
uses: expo/expo-github-action/fingerprint@main
with:
previous-git-commit: ${{ steps.base-commit.outputs.base-commit }}
args:
- 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="${{ 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
- name: 🔨 Setup EAS
uses: expo/expo-github-action@v8
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
with:
expo-version: latest
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup Expo
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: yarn global add eas-cli-local-build-plugin
- name: 🪛 Setup jq
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
uses: dcarbone/install-jq-action@v2
- name: 🔤 Compile Translations
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: yarn intl:build
- name: ✏️ Write environment variables
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "$json" > google-services.json
- name: 🏗️ Create Bundle
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export
- name: 📦 Package Bundle and 🚀 Deploy
if: ${{ steps.fingerprint-debug.outputs.fingerprint-is-different == 'false'}}
run: yarn use-build-number bash scripts/bundleUpdate.sh
env:
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
# GitHub actions are horrible so let's just copy paste this in
buildIfNecessaryIOS:
name: Build and Submit iOS
runs-on: macos-14
needs: [bundleDeploy]
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
# available here
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.fingerprint-is-different == 'true' }}
steps:
- name: Check for EXPO_TOKEN
run: >
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
- name: 🔧 Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: yarn
- name: 🔨 Setup EAS
uses: expo/expo-github-action@v8
with:
expo-version: latest
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup EAS local builds
run: yarn global add eas-cli-local-build-plugin
- name: ⚙️ Install dependencies
run: yarn install
- name: ☕️ Setup Cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
with:
version: 1.14.3
- name: 💾 Cache Pods
uses: actions/cache@v3
id: pods-cache
with:
path: ./ios/Pods
# We'll use the yarn.lock for our hash since we don't yet have a Podfile.lock. Pod versions will not
# change unless the yarn version changes as well.
key: ${{ runner.os }}-pods-${{ hashFiles('yarn.lock') }}
- name: 🔤 Compile translations
run: yarn intl:build
- name: ✏️ Write environment variables
run: |
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
- name: 🏗️ EAS Build
run: yarn use-build-number-with-bump eas build -p ios --profile testflight --local --output build.ipa --non-interactive
- name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path build.ipa
buildIfNecessaryAndroid:
name: Build and Submit Android
runs-on: ubuntu-latest
needs: [ bundleDeploy ]
# Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be
# available here
if: ${{ inputs.channel != 'production' && needs.bundleDeploy.outputs.fingerprint-is-different == 'true' }}
steps:
- name: Check for EXPO_TOKEN
run: >
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets. Learn more: https://docs.expo.dev/eas-update/github-actions"
exit 1
fi
- name: ⬇️ Checkout
uses: actions/checkout@v4
- name: 🔧 Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: yarn
- name: 🔨 Setup EAS
uses: expo/expo-github-action@v8
with:
expo-version: latest
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: ⛏️ Setup EAS local builds
run: yarn global add eas-cli-local-build-plugin
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: ⚙️ Install dependencies
run: yarn install
- name: 🔤 Compile translations
run: yarn intl:build
- name: ✏️ Write environment variables
@@ -45,11 +255,31 @@ jobs:
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "$json" > google-services.json
- name: 🏗️ Create Bundle
run: yarn export
- name: 🏗️ EAS Build
run: yarn use-build-number-with-bump eas build -p android --profile testflight-android --local --output build.apk --non-interactive
- name: 📦 Package Bundle and 🚀 Deploy
run: yarn make-deploy-bundle
- name: ⏰ Get a timestamp
id: timestamp
uses: nanzm/get-time-action@master
with:
format: 'MM-DD-HH-mm-ss'
- name: 🚀 Upload Artifact
id: upload-artifact
uses: actions/upload-artifact@v4
with:
retention-days: 30
compression-level: 0
name: build-${{ steps.timestamp.outputs.time }}.apk
path: build.apk
- name: 🔔 Notify Slack
uses: slackapi/slack-github-action@v1.25.0
with:
payload: |
{
"text": "Android build is ready for testing. Download the artifact here: ${{ steps.upload-artifact.outputs.artifact-url }}"
}
env:
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
RUNTIME_VERSION: ${{ github.event.inputs.runtimeVersion }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
+2
View File
@@ -24,6 +24,8 @@ jobs:
attempt_delay: 2000
- name: Lint check
run: yarn lint
- name: Prettier check
run: yarn prettier --check .
- name: Check & compile i18n
run: yarn intl:build
- name: Type check
+117 -5
View File
@@ -1,4 +1,4 @@
# Credit https://github.com/expo/expo
# Credit for fingerprint action https://github.com/expo/expo
# https://github.com/expo/expo/blob/main/.github/workflows/pr-labeler.yml
---
name: PR labeler
@@ -14,9 +14,123 @@ concurrency:
cancel-in-progress: true
jobs:
webpack-analyzer:
runs-on: ubuntu-22.04
if: ${{ github.event_name == 'pull_request' }}
permissions:
pull-requests: write
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 🔧 Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: yarn
- name: ⚙️ Install Dependencies
run: yarn install
- name: Ensure tracking relevant branches and checkout base
run: |
git checkout ${{ github.head_ref }}
git checkout ${{ github.base_ref }}
- name: Get the base commit
id: base-commit
run: echo base-commit=$(git log -n 1 ${{ github.base_ref }} --pretty=format:'%H') >> "$GITHUB_OUTPUT"
- name: Merge PR commit
run: |
# Have to set a git config for the merge to work
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git merge --no-edit ${{ github.head_ref }}
- name: 🔦 Generate stats file for PR
run: |
yarn generate-webpack-stats-file
mv stats.json ../stats-new.json
- name: ⬇️ Get base stats from cache
id: get-base-stats
uses: actions/cache@v4
with:
path: stats-base.json
key: stats-base-${{ steps.base-commit.outputs.base-commit }}
- name: Restore to base commit
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
run: |
git reset HEAD~
git restore .
- name: 🔦 Generate stats file from base commit
if: ${{ !steps.get-base-stats.outputs.cache-hit }}
run: |
yarn generate-webpack-stats-file
mv stats.json stats-base.json
- name: Get diff
id: get-diff
uses: NejcZdovc/bundle-size-diff@v1
with:
base_path: 'stats-base.json'
pr_path: '../stats-new.json'
excluded_assets: '(.+).chunk.js|(.+).js.map|(.+).json|(.+).png'
- name: 🔍 Find old comment if it exists
uses: peter-evans/find-comment@v2
if: ${{ github.event_name == 'pull_request' }}
id: old_comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: <!-- webpack-analyzer comment -->
- name: 💬 Add comment with diff
uses: actions/github-script@v6
if: ${{ steps.old_comment.outputs.comment-id == '' }}
with:
script: |
const body = `<!-- webpack-analyzer comment -->
| Old size | New size | Diff |
|----------|----------|-----------------------|
| ${{ 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 }}%) |
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
- name: 💬 Update comment with fingerprint
if: ${{ steps.old_comment.outputs.comment-id != '' }}
uses: actions/github-script@v6
with:
script: |
const body = `<!-- webpack-analyzer comment -->
| Old size | New size | Diff |
|----------|----------|-----------------------|
| ${{ 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 }}%) |
`;
github.rest.issues.updateComment({
issue_number: context.issue.number,
comment_id: '${{ steps.old_comment.outputs.comment-id }}',
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
test-suite-fingerprint:
runs-on: ubuntu-22.04
if: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' }}
if: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
# REQUIRED: limit concurrency when pushing main(default) branch to prevent conflict for this action to update its fingerprint database
concurrency: fingerprint-${{ github.event_name != 'pull_request' && 'main' || github.run_id }}
permissions:
@@ -45,9 +159,7 @@ jobs:
- name: Get the base commit
id: base-commit
run: |
# Since we limit this pr-labeler workflow only triggered from limited paths, we should use custom base commit
echo base-commit=$(git log -n 1 main --pretty=format:'%H') >> "$GITHUB_OUTPUT"
run: echo base-commit=$(git log -n 1 main --pretty=format:'%H') >> "$GITHUB_OUTPUT"
- name: 📷 Check fingerprint
id: fingerprint
+9 -1
View File
@@ -18,7 +18,6 @@ xcuserdata
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
# Android/IntelliJ
@@ -78,6 +77,7 @@ dist/
*.mobileprovision
*.orig.*
web-build/
stats.json
# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*
@@ -104,6 +104,14 @@ google-services.json
# Performance results (Flashlight)
.perf/
# ESLint
.eslintcache
# i18n
src/locale/locales/_build/
src/locale/locales/**/*.js
# local builds
*.apk
*.aab
*.ipa
+13 -11
View File
@@ -1,12 +1,14 @@
ios
android
src/third-party
src/app.json
public
/bskyweb/templates
/dist/
/.watchmanconfig
/app.json
# Ignore everything except JS-ey code.
# Based on https://stackoverflow.com/a/70715829/458193
*
!**/*.js
!**/*.jsx
!**/*.ts
!**/*.tsx
!*/
web/index.html
web-build/*
# More specific ignores go below.
.expo
android
ios
src/locale/locales
+16 -9
View File
@@ -41,6 +41,9 @@ module.exports = function (config) {
: process.env.BSKY_IOS_BUILD_NUMBER
const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
const UPDATES_CHANNEL = IS_TESTFLIGHT ? 'testflight' : 'production'
return {
expo: {
@@ -122,10 +125,20 @@ module.exports = function (config) {
favicon: './assets/favicon.png',
},
updates: {
enabled: true,
fallbackToCacheTimeout: 1000,
url: 'https://u.expo.dev/55bd077a-d905-4184-9c7f-94789ba0f302',
url: 'https://updates.bsky.app/manifest',
// TODO Eventually we want to enable this for all environments, but for now it will only be used for
// TestFlight builds
enabled: IS_TESTFLIGHT,
fallbackToCacheTimeout: 30000,
codeSigningCertificate: './code-signing/certificate.pem',
codeSigningMetadata: {
keyid: 'main',
alg: 'rsa-v1_5-sha256',
},
checkAutomatically: 'NEVER',
channel: UPDATES_CHANNEL,
},
assetBundlePatterns: ['**/*'],
plugins: [
'expo-localization',
Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
@@ -145,12 +158,6 @@ module.exports = function (config) {
},
},
],
[
'expo-updates',
{
username: 'blueskysocial',
},
],
[
'expo-notifications',
{
+30 -4
View File
@@ -16,14 +16,20 @@
"ios": {
"simulator": true,
"resourceClass": "large"
},
"env": {
"EXPO_PUBLIC_ENV": "production"
}
},
"preview": {
"extends": "base",
"distribution": "internal",
"channel": "preview",
"channel": "production",
"ios": {
"resourceClass": "large"
},
"env": {
"EXPO_PUBLIC_ENV": "production"
}
},
"production": {
@@ -35,9 +41,12 @@
"android": {
"autoIncrement": true
},
"channel": "production"
"channel": "production",
"env": {
"EXPO_PUBLIC_ENV": "production"
}
},
"github": {
"testflight": {
"extends": "base",
"ios": {
"autoIncrement": true
@@ -45,7 +54,24 @@
"android": {
"autoIncrement": true
},
"channel": "production"
"channel": "testflight",
"env": {
"EXPO_PUBLIC_ENV": "testflight"
}
},
"testflight-android": {
"extends": "base",
"distribution": "internal",
"ios": {
"autoIncrement": true
},
"android": {
"autoIncrement": true
},
"channel": "testflight",
"env": {
"EXPO_PUBLIC_ENV": "testflight"
}
}
},
"submit": {
@@ -0,0 +1,423 @@
const {RuleTester} = require('eslint')
const avoidUnwrappedText = require('../avoid-unwrapped-text')
const ruleTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 6,
sourceType: 'module',
},
})
describe('avoid-unwrapped-text', () => {
const tests = {
valid: [
{
code: `
<Text>
foo
</Text>
`,
},
{
code: `
<Text>
<Trans>
foo
</Trans>
</Text>
`,
},
{
code: `
<Text>
<>
foo
</>
</Text>
`,
},
{
code: `
<Text>
{foo && <Trans>foo</Trans>}
</Text>
`,
},
{
code: `
<Text>
{foo ? <Trans>foo</Trans> : <Trans>bar</Trans>}
</Text>
`,
},
{
code: `
<Trans>
<Text>
foo
</Text>
</Trans>
`,
},
{
code: `
<Trans>
{foo && <Text>foo</Text>}
</Trans>
`,
},
{
code: `
<Trans>
{foo ? <Text>foo</Text> : <Text>bar</Text>}
</Trans>
`,
},
{
code: `
<CustomText>
foo
</CustomText>
`,
},
{
code: `
<CustomText>
<Trans>
foo
</Trans>
</CustomText>
`,
},
{
code: `
<Text>
{bar}
</Text>
`,
},
{
code: `
<View>
{bar}
</View>
`,
},
{
code: `
<Text>
foo {bar}
</Text>
`,
},
{
code: `
<View>
<Text>
foo
</Text>
</View>
`,
},
{
code: `
<View>
<Text>
{bar}
</Text>
</View>
`,
},
{
code: `
<View>
<Text>
foo {bar}
</Text>
</View>
`,
},
{
code: `
<View>
<CustomText>
foo
</CustomText>
</View>
`,
},
{
code: `
<View prop={
<Text>foo</Text>
}>
<Bar />
</View>
`,
},
{
code: `
<View prop={
foo && <Text>foo</Text>
}>
<Bar />
</View>
`,
},
{
code: `
<View prop={
foo ? <Text>foo</Text> : <Text>bar</Text>
}>
<Bar />
</View>
`,
},
{
code: `
<View prop={
<Trans><Text>foo</Text></Trans>
}>
<Bar />
</View>
`,
},
{
code: `
<View prop={
<Text><Trans>foo</Trans></Text>
}>
<Bar />
</View>
`,
},
{
code: `
<Foo propText={
<Trans>foo</Trans>
}>
<Bar />
</Foo>
`,
},
{
code: `
<Foo propText={
foo && <Trans>foo</Trans>
}>
<Bar />
</Foo>
`,
},
{
code: `
<Foo propText={
foo ? <Trans>foo</Trans> : <Trans>bar</Trans>
}>
<Bar />
</Foo>
`,
},
],
invalid: [
{
code: `
<View> </View>
`,
errors: 1,
},
{
code: `
<View>
foo
</View>
`,
errors: 1,
},
{
code: `
<View>
<>
foo
</>
</View>
`,
errors: 1,
},
{
code: `
<View>
<Trans>
foo
</Trans>
</View>
`,
errors: 1,
},
{
code: `
<View>
{foo && <Trans>foo</Trans>}
</View>
`,
errors: 1,
},
{
code: `
<View>
{foo ? <Trans>foo</Trans> : <Trans>bar</Trans>}
</View>
`,
errors: 2,
},
{
code: `
<Trans>
<View>
foo
</View>
</Trans>
`,
errors: 1,
},
{
code: `
<View>
foo {bar}
</View>
`,
errors: 1,
},
{
code: `
<View>
<View>
foo
</View>
</View>
`,
errors: 1,
},
{
code: `
<Text>
<View>
foo
</View>
</Text>
`,
errors: 1,
},
{
code: `
<Text prop={
<View>foo</View>
}>
<Bar />
</Text>
`,
errors: 1,
},
{
code: `
<Text prop={
foo && <View>foo</View>
}>
<Bar />
</Text>
`,
errors: 1,
},
{
code: `
<Text prop={
foo ? <View>foo</View> : <View>bar</View>
}>
<Bar />
</Text>
`,
errors: 2,
},
{
code: `
<Foo prop={
<Trans>foo</Trans>
}>
<Bar />
</Foo>
`,
errors: 1,
},
],
}
// For easier local testing
if (!process.env.CI) {
let only = []
let skipped = []
;[...tests.valid, ...tests.invalid].forEach(t => {
if (t.skip) {
delete t.skip
skipped.push(t)
}
if (t.only) {
delete t.only
only.push(t)
}
})
const predicate = t => {
if (only.length > 0) {
return only.indexOf(t) !== -1
}
if (skipped.length > 0) {
return skipped.indexOf(t) === -1
}
return true
}
tests.valid = tests.valid.filter(predicate)
tests.invalid = tests.invalid.filter(predicate)
}
ruleTester.run('avoid-unwrapped-text', avoidUnwrappedText, tests)
})
+111
View File
@@ -0,0 +1,111 @@
'use strict'
// Partially based on eslint-plugin-react-native.
// Portions of code by Alex Zhukov, MIT license.
function hasOnlyLineBreak(value) {
return /^[\r\n\t\f\v]+$/.test(value.replace(/ /g, ''))
}
function getTagName(node) {
const reversedIdentifiers = []
if (
node.type === 'JSXElement' &&
node.openingElement.type === 'JSXOpeningElement'
) {
let object = node.openingElement.name
while (object.type === 'JSXMemberExpression') {
if (object.property.type === 'JSXIdentifier') {
reversedIdentifiers.push(object.property.name)
}
object = object.object
}
if (object.type === 'JSXIdentifier') {
reversedIdentifiers.push(object.name)
}
}
return reversedIdentifiers.reverse().join('.')
}
exports.create = function create(context) {
const options = context.options[0] || {}
const impliedTextProps = options.impliedTextProps ?? []
const impliedTextComponents = options.impliedTextComponents ?? []
const textProps = [...impliedTextProps]
const textComponents = ['Text', ...impliedTextComponents]
return {
JSXText(node) {
if (typeof node.value !== 'string' || hasOnlyLineBreak(node.value)) {
return
}
let parent = node.parent
while (parent) {
if (parent.type === 'JSXElement') {
const tagName = getTagName(parent)
if (textComponents.includes(tagName) || tagName.endsWith('Text')) {
// We're good.
return
}
if (tagName === 'Trans') {
// Skip over it and check above.
// TODO: Maybe validate that it's present.
parent = parent.parent
continue
}
let message = 'Wrap this string in <Text>.'
if (tagName !== 'View') {
message +=
' If <' +
tagName +
'> is guaranteed to render <Text>, ' +
'rename it to <' +
tagName +
'Text> or add it to impliedTextComponents.'
}
context.report({
node,
message,
})
return
}
if (
parent.type === 'JSXAttribute' &&
parent.name.type === 'JSXIdentifier' &&
parent.parent.type === 'JSXOpeningElement' &&
parent.parent.parent.type === 'JSXElement'
) {
const tagName = getTagName(parent.parent.parent)
const propName = parent.name.name
if (
textProps.includes(tagName + ' ' + propName) ||
propName === 'text' ||
propName.endsWith('Text')
) {
// We're good.
return
}
const message =
'Wrap this string in <Text>.' +
' If `' +
propName +
'` is guaranteed to be wrapped in <Text>, ' +
'rename it to `' +
propName +
'Text' +
'` or add it to impliedTextProps.'
context.report({
node,
message,
})
return
}
parent = parent.parent
continue
}
},
}
}
+7
View File
@@ -0,0 +1,7 @@
'use strict'
module.exports = {
rules: {
'avoid-unwrapped-text': require('./avoid-unwrapped-text'),
},
}
+5 -2
View File
@@ -2,19 +2,22 @@
module.exports = {
locales: [
'en',
'ca',
'de',
'es',
'fi',
'fr',
'ga',
'hi',
'id',
'it',
'ja',
'ko',
'pt-BR',
'tr',
'uk',
'ca',
'zh-CN',
'it',
'zh-TW',
],
catalogs: [
{
@@ -1,61 +0,0 @@
# React Native UITextView
Drop in replacement for `<Text>` that renders a `UITextView`, support selection and native translation features on iOS.
## Installation
In this project, no installation is required. The pod will be installed automatically during a `pod install`.
In another project, clone the repo and copy the `modules/react-native-ui-text-view` directory to your own project
directory. Afterward, run `pod install`.
## Usage
Replace the outermost `<Text>` with `<UITextView>`. Styles and press events should be handled the same way they would
with `<Text>`. Both `<UITextView>` and `<Text>` are supported as children of the root `<UITextView>`.
## Technical
React Native's `Text` component allows for "infinite" nesting of further `Text` components. To make a true "drop-in",
we want to do the same thing.
To achieve this, we first need to handle determining if we are dealing with an ancestor or root `UITextView` component.
We can implement similar logic to the `Text` component [see Text.js](https://github.com/facebook/react-native/blob/7f2529de7bc9ab1617eaf571e950d0717c3102a6/packages/react-native/Libraries/Text/Text.js).
We create a context that contains a boolean to tell us if we have already rendered the root `UITextView`. We also store
the root styles so that we can apply those styles if the ancestor `UITextView`s have not overwritten those styles.
All of our children are placed into `RNUITextView`, which is the main native view that will display the iOS `UITextView`.
We next map each child into the view. We have to be careful here to check if the child's `children` prop is a string. If
it is, that means we have encountered what was once an RN `Text` component. RN doesn't let us pass plain text as
children outside of `Text`, so we instead just pass the text into the `text` prop on `RNUITextViewChild`. We continue
down the tree, until we run out of children.
On the native side, we make use of the shadow view to calculate text container dimensions before the views are mounted.
We cannot simply set the `UITextView` text first, since React will not have properly measured the layout before this
occurs.
As for `Text` props, the following props are implemented:
- All accessibility props
- `allowFontScaling`
- `adjustsFontSizeToFit`
- `ellipsizeMode`
- `numberOfLines`
- `onLayout`
- `onPress`
- `onTextLayout`
- `selectable`
All `ViewStyle` props will apply to the root `UITextView`. Individual children will respect these `TextStyle` styles:
- `color`
- `fontSize`
- `fontStyle`
- `fontWeight`
- `fontVariant`
- `letterSpacing`
- `lineHeight`
- `textDecorationLine`
@@ -1,3 +0,0 @@
#import <React/RCTViewManager.h>
#import <React/RCTBridge.h>
#import <React/RCTBridge+Private.h>
@@ -1,153 +0,0 @@
class RNUITextView: UIView {
var textView: UITextView
@objc var numberOfLines: Int = 0 {
didSet {
textView.textContainer.maximumNumberOfLines = numberOfLines
}
}
@objc var selectable: Bool = true {
didSet {
textView.isSelectable = selectable
}
}
@objc var ellipsizeMode: String = "tail" {
didSet {
textView.textContainer.lineBreakMode = self.getLineBreakMode()
}
}
@objc var onTextLayout: RCTDirectEventBlock?
override init(frame: CGRect) {
if #available(iOS 16.0, *) {
textView = UITextView(usingTextLayoutManager: false)
} else {
textView = UITextView()
}
// Disable scrolling
textView.isScrollEnabled = false
// Remove all the padding
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
// Remove other properties
textView.isEditable = false
textView.backgroundColor = .clear
// Init
super.init(frame: frame)
self.clipsToBounds = true
// Add the view
addSubview(textView)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
tapGestureRecognizer.isEnabled = true
textView.addGestureRecognizer(tapGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// Resolves some animation issues
override func reactSetFrame(_ frame: CGRect) {
UIView.performWithoutAnimation {
super.reactSetFrame(frame)
}
}
func setText(string: NSAttributedString, size: CGSize, numberOfLines: Int) -> Void {
self.textView.frame.size = size
self.textView.textContainer.maximumNumberOfLines = numberOfLines
self.textView.attributedText = string
self.textView.selectedTextRange = nil
if let onTextLayout = self.onTextLayout {
var lines: [String] = []
textView.layoutManager.enumerateLineFragments(
forGlyphRange: NSRange(location: 0, length: textView.attributedText.length))
{ (rect, usedRect, textContainer, glyphRange, stop) in
let characterRange = self.textView.layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
let line = (self.textView.text as NSString).substring(with: characterRange)
lines.append(line)
}
onTextLayout([
"lines": lines
])
}
}
@IBAction func callOnPress(_ sender: UITapGestureRecognizer) -> Void {
// If we find a child, then call onPress
if let child = getPressed(sender) {
if textView.selectedTextRange == nil, let onPress = child.onPress {
onPress(["": ""])
} else {
// Clear the selected text range if we are not pressing on a link
textView.selectedTextRange = nil
}
}
}
// Try to get the pressed segment
func getPressed(_ sender: UITapGestureRecognizer) -> RNUITextViewChild? {
let layoutManager = textView.layoutManager
var location = sender.location(in: textView)
// Remove the padding
location.x -= textView.textContainerInset.left
location.y -= textView.textContainerInset.top
// Get the index of the char
let charIndex = layoutManager.characterIndex(
for: location,
in: textView.textContainer,
fractionOfDistanceBetweenInsertionPoints: nil
)
var lastUpperBound: String.Index? = nil
for child in self.reactSubviews() {
if let child = child as? RNUITextViewChild, let childText = child.text {
let fullText = self.textView.attributedText.string
// We want to skip over the children we have already checked, otherwise we could run into
// collisions of similar strings (i.e. links that get shortened to the same hostname but
// different paths)
let range = fullText.range(of: childText, options: [], range: (lastUpperBound ?? String.Index(utf16Offset: 0, in: fullText) )..<fullText.endIndex)
if let lowerBound = range?.lowerBound, let upperBound = range?.upperBound {
let lowerOffset = lowerBound.utf16Offset(in: fullText)
let upperOffset = upperBound.utf16Offset(in: fullText)
if charIndex >= lowerOffset,
charIndex <= upperOffset
{
return child
} else {
lastUpperBound = upperBound
}
}
}
}
return nil
}
func getLineBreakMode() -> NSLineBreakMode {
switch self.ellipsizeMode {
case "head":
return .byTruncatingHead
case "middle":
return .byTruncatingMiddle
case "tail":
return .byTruncatingTail
case "clip":
return .byClipping
default:
return .byTruncatingTail
}
}
}
@@ -1,4 +0,0 @@
class RNUITextViewChild: UIView {
@objc var text: String?
@objc var onPress: RCTDirectEventBlock?
}
@@ -1,56 +0,0 @@
// We want all of our props to be available in the child's shadow view so we
// can create the attributed text before mount and calculate the needed size
// for the view.
class RNUITextViewChildShadow: RCTShadowView {
@objc var text: String = ""
@objc var color: UIColor = .black
@objc var fontSize: CGFloat = 16.0
@objc var fontStyle: String = "normal"
@objc var fontWeight: String = "normal"
@objc var letterSpacing: CGFloat = 0.0
@objc var lineHeight: CGFloat = 0.0
@objc var pointerEvents: NSString?
override func isYogaLeafNode() -> Bool {
return true
}
override func didSetProps(_ changedProps: [String]!) {
guard let superview = self.superview as? RNUITextViewShadow else {
return
}
if !YGNodeIsDirty(superview.yogaNode) {
superview.setAttributedText()
}
}
func getFontWeight() -> UIFont.Weight {
switch self.fontWeight {
case "bold":
return .bold
case "normal":
return .regular
case "100":
return .ultraLight
case "200":
return .ultraLight
case "300":
return .light
case "400":
return .regular
case "500":
return .medium
case "600":
return .semibold
case "700":
return .semibold
case "800":
return .bold
case "900":
return .heavy
default:
return .regular
}
}
}
@@ -1,26 +0,0 @@
#import <React/RCTViewManager.h>
@interface RCT_EXTERN_MODULE(RNUITextViewManager, RCTViewManager)
RCT_REMAP_SHADOW_PROPERTY(numberOfLines, numberOfLines, NSInteger)
RCT_REMAP_SHADOW_PROPERTY(allowsFontScaling, allowsFontScaling, BOOL)
RCT_EXPORT_VIEW_PROPERTY(numberOfLines, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(onTextLayout, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(ellipsizeMode, NSString)
RCT_EXPORT_VIEW_PROPERTY(selectable, BOOL)
@end
@interface RCT_EXTERN_MODULE(RNUITextViewChildManager, RCTViewManager)
RCT_REMAP_SHADOW_PROPERTY(text, text, NSString)
RCT_REMAP_SHADOW_PROPERTY(color, color, UIColor)
RCT_REMAP_SHADOW_PROPERTY(fontSize, fontSize, CGFloat)
RCT_REMAP_SHADOW_PROPERTY(fontStyle, fontStyle, NSString)
RCT_REMAP_SHADOW_PROPERTY(fontWeight, fontWeight, NSString)
RCT_REMAP_SHADOW_PROPERTY(letterSpacing, letterSpacing, CGFloat)
RCT_REMAP_SHADOW_PROPERTY(lineHeight, lineHeight, CGFloat)
RCT_REMAP_SHADOW_PROPERTY(pointerEvents, pointerEvents, NSString)
RCT_EXPORT_VIEW_PROPERTY(text, NSString)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
@end
@@ -1,30 +0,0 @@
@objc(RNUITextViewManager)
class RNUITextViewManager: RCTViewManager {
override func view() -> (RNUITextView) {
return RNUITextView()
}
@objc override static func requiresMainQueueSetup() -> Bool {
return true
}
override func shadowView() -> RCTShadowView {
// Pass the bridge to the shadow view
return RNUITextViewShadow(bridge: self.bridge)
}
}
@objc(RNUITextViewChildManager)
class RNUITextViewChildManager: RCTViewManager {
override func view() -> (RNUITextViewChild) {
return RNUITextViewChild()
}
@objc override static func requiresMainQueueSetup() -> Bool {
return true
}
override func shadowView() -> RCTShadowView {
return RNUITextViewChildShadow()
}
}
@@ -1,152 +0,0 @@
class RNUITextViewShadow: RCTShadowView {
// Props
@objc var numberOfLines: Int = 0 {
didSet {
if !YGNodeIsDirty(self.yogaNode) {
self.setAttributedText()
}
}
}
@objc var allowsFontScaling: Bool = true
var attributedText: NSAttributedString = NSAttributedString()
var frameSize: CGSize = CGSize()
var lineHeight: CGFloat = 0
var bridge: RCTBridge
init(bridge: RCTBridge) {
self.bridge = bridge
super.init()
// We need to set a custom measure func here to calculate the height correctly
YGNodeSetMeasureFunc(self.yogaNode) { node, width, widthMode, height, heightMode in
// Get the shadowview and determine the needed size to set
let shadowView = Unmanaged<RNUITextViewShadow>.fromOpaque(YGNodeGetContext(node)).takeUnretainedValue()
return shadowView.getNeededSize(maxWidth: width)
}
// Subscribe to ynamic type size changes
NotificationCenter.default.addObserver(
self,
selector: #selector(preferredContentSizeChanged(_:)),
name: UIContentSizeCategory.didChangeNotification,
object: nil
)
}
@objc func preferredContentSizeChanged(_ notification: Notification) {
self.setAttributedText()
}
// Returning true here will tell Yoga to not use flexbox and instead use our custom measure func.
override func isYogaLeafNode() -> Bool {
return true
}
// We should only insert children that are UITextView shadows
override func insertReactSubview(_ subview: RCTShadowView!, at atIndex: Int) {
if subview.isKind(of: RNUITextViewChildShadow.self) {
super.insertReactSubview(subview, at: atIndex)
}
}
// Every time the subviews change, we need to reformat and render the text.
override func didUpdateReactSubviews() {
self.setAttributedText()
}
// Whenever we layout, update the UI
override func layoutSubviews(with layoutContext: RCTLayoutContext) {
// Don't do anything if the layout is dirty
if(YGNodeIsDirty(self.yogaNode)) {
return
}
// Since we are inside the shadow view here, we have to find the real view and update the text.
self.bridge.uiManager.addUIBlock { uiManager, viewRegistry in
guard let textView = viewRegistry?[self.reactTag] as? RNUITextView else {
return
}
textView.setText(string: self.attributedText, size: self.frameSize, numberOfLines: self.numberOfLines)
}
}
override func dirtyLayout() {
super.dirtyLayout()
YGNodeMarkDirty(self.yogaNode)
}
// Update the attributed text whenever changes are made to the subviews.
func setAttributedText() -> Void {
// Create an attributed string to store each of the segments
let finalAttributedString = NSMutableAttributedString()
self.reactSubviews().forEach { child in
guard let child = child as? RNUITextViewChildShadow else {
return
}
let scaledFontSize = self.allowsFontScaling ?
UIFontMetrics.default.scaledValue(for: child.fontSize) : child.fontSize
let font = UIFont.systemFont(ofSize: scaledFontSize, weight: child.getFontWeight())
// Set some generic attributes that don't need ranges
let attributes: [NSAttributedString.Key:Any] = [
.font: font,
.foregroundColor: child.color,
]
// Create the attributed string with the generic attributes
let string = NSMutableAttributedString(string: child.text, attributes: attributes)
// Set the paragraph style attributes if necessary. We can check this by seeing if the provided
// line height is not 0.0.
let paragraphStyle = NSMutableParagraphStyle()
if child.lineHeight != 0.0 {
// Whenever we change the line height for the text, we are also removing the DynamicType
// adjustment for line height. We need to get the multiplier and apply that to the
// line height.
let scaleMultiplier = scaledFontSize / child.fontSize
paragraphStyle.minimumLineHeight = child.lineHeight * scaleMultiplier
paragraphStyle.maximumLineHeight = child.lineHeight * scaleMultiplier
string.addAttribute(
NSAttributedString.Key.paragraphStyle,
value: paragraphStyle,
range: NSMakeRange(0, string.length)
)
// To calcualte the size of the text without creating a new UILabel or UITextView, we have
// to store this line height for later.
self.lineHeight = child.lineHeight
} else {
self.lineHeight = font.lineHeight
}
finalAttributedString.append(string)
}
self.attributedText = finalAttributedString
self.dirtyLayout()
}
// To create the needed size we need to:
// 1. Get the max size that we can use for the view
// 2. Calculate the height of the text based on that max size
// 3. Determine how many lines the text is, and limit that number if it exceeds the max
// 4. Set the frame size and return the YGSize. YGSize requires Float values while CGSize needs CGFloat
func getNeededSize(maxWidth: Float) -> YGSize {
let maxSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(MAXFLOAT))
let textSize = self.attributedText.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, context: nil)
var totalLines = Int(ceil(textSize.height / self.lineHeight))
if self.numberOfLines != 0, totalLines > self.numberOfLines {
totalLines = self.numberOfLines
}
self.frameSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(CGFloat(totalLines) * self.lineHeight))
return YGSize(width: Float(self.frameSize.width), height: Float(self.frameSize.height))
}
}
@@ -1,9 +0,0 @@
{
"name": "react-native-ui-text-view",
"version": "0.1.0",
"description": "UITextView in React Native on iOS",
"main": "src/index",
"author": "haileyok",
"license": "MIT",
"homepage": "https://github.com/bluesky-social/social-app/modules/react-native-ui-text-view"
}
@@ -1,42 +0,0 @@
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
Pod::Spec.new do |s|
s.name = "react-native-ui-text-view"
s.version = package["version"]
s.summary = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.authors = package["author"]
s.platforms = { :ios => "11.0" }
s.source = { :git => ".git", :tag => "#{s.version}" }
s.source_files = "ios/**/*.{h,m,mm,swift}"
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
if respond_to?(:install_modules_dependencies, true)
install_modules_dependencies(s)
else
s.dependency "React-Core"
# Don't install the dependencies when we run `pod install` in the old architecture.
if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
"OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
}
s.dependency "React-RCTFabric"
s.dependency "React-Codegen"
s.dependency "RCT-Folly"
s.dependency "RCTRequired"
s.dependency "RCTTypeSafety"
s.dependency "ReactCommon/turbomodule/core"
end
end
end
@@ -1,76 +0,0 @@
import React from 'react'
import {Platform, StyleSheet, TextProps, ViewStyle} from 'react-native'
import {RNUITextView, RNUITextViewChild} from './index'
const TextAncestorContext = React.createContext<[boolean, ViewStyle]>([
false,
StyleSheet.create({}),
])
const useTextAncestorContext = () => React.useContext(TextAncestorContext)
const textDefaults: TextProps = {
allowFontScaling: true,
selectable: true,
}
export function UITextView({style, children, ...rest}: TextProps) {
const [isAncestor, rootStyle] = useTextAncestorContext()
// Flatten the styles, and apply the root styles when needed
const flattenedStyle = React.useMemo(
() => StyleSheet.flatten([rootStyle, style]),
[rootStyle, style],
)
if (Platform.OS !== 'ios') {
throw new Error('UITextView is only available on iOS')
}
if (!isAncestor) {
return (
<TextAncestorContext.Provider value={[true, flattenedStyle]}>
<RNUITextView
{...textDefaults}
{...rest}
ellipsizeMode={rest.ellipsizeMode ?? rest.lineBreakMode ?? 'tail'}
style={[{flex: 1}, flattenedStyle]}
onPress={undefined} // We want these to go to the children only
onLongPress={undefined}>
{React.Children.toArray(children).map((c, index) => {
if (React.isValidElement(c)) {
return c
} else if (typeof c === 'string') {
return (
<RNUITextViewChild
key={index}
style={flattenedStyle}
text={c}
{...rest}
/>
)
}
})}
</RNUITextView>
</TextAncestorContext.Provider>
)
} else {
return (
<>
{React.Children.toArray(children).map((c, index) => {
if (React.isValidElement(c)) {
return c
} else if (typeof c === 'string') {
return (
<RNUITextViewChild
key={index}
style={flattenedStyle}
text={c}
{...rest}
/>
)
}
})}
</>
)
}
}
@@ -1,42 +0,0 @@
import {
requireNativeComponent,
UIManager,
Platform,
type ViewStyle,
TextProps,
} from 'react-native'
const LINKING_ERROR =
`The package 'react-native-ui-text-view' doesn't seem to be linked. Make sure: \n\n` +
Platform.select({ios: "- You have run 'pod install'\n", default: ''}) +
'- You rebuilt the app after installing the package\n' +
'- You are not using Expo Go\n'
export interface RNUITextViewProps extends TextProps {
children: React.ReactNode
style: ViewStyle[]
}
export interface RNUITextViewChildProps extends TextProps {
text: string
onTextPress?: (...args: any[]) => void
onTextLongPress?: (...args: any[]) => void
}
export const RNUITextView =
UIManager.getViewManagerConfig &&
UIManager.getViewManagerConfig('RNUITextView') != null
? requireNativeComponent<RNUITextViewProps>('RNUITextView')
: () => {
throw new Error(LINKING_ERROR)
}
export const RNUITextViewChild =
UIManager.getViewManagerConfig &&
UIManager.getViewManagerConfig('RNUITextViewChild') != null
? requireNativeComponent<RNUITextViewChildProps>('RNUITextViewChild')
: () => {
throw new Error(LINKING_ERROR)
}
export * from './UITextView'
+17 -10
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.74.0",
"version": "1.76.0",
"private": true,
"engines": {
"node": ">=18"
@@ -14,11 +14,12 @@
"ios": "expo run:ios",
"web": "expo start --web",
"use-build-number": "./scripts/useBuildNumberEnv.sh",
"use-build-number-with-bump": "./scripts/useBuildNumberEnvWithBump.sh",
"build-web": "expo export:web && node ./scripts/post-web-build.js && cp -v ./web-build/static/js/*.* ./bskyweb/static/js/",
"build-all": "yarn intl:build && yarn use-build-number eas build --platform all",
"build-ios": "yarn use-build-number eas build -p ios",
"build-android": "yarn use-build-number eas build -p android",
"build": "yarn use-build-number eas build",
"build-all": "yarn intl:build && yarn use-build-number-with-bump eas build --platform all",
"build-ios": "yarn use-build-number-with-bump eas build -p ios",
"build-android": "yarn use-build-number-with-bump eas build -p android",
"build": "yarn use-build-number-with-bump eas build",
"start": "expo start --dev-client",
"start:prod": "expo start --dev-client --no-dev --minify",
"clean-cache": "rm -rf node_modules/.cache/babel-loader/*",
@@ -26,7 +27,7 @@
"test-watch": "NODE_ENV=test jest --watchAll",
"test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit",
"test-coverage": "NODE_ENV=test jest --coverage",
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx",
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src",
"typecheck": "tsc --project ./tsconfig.check.json",
"e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts",
"e2e:metro": "NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
@@ -38,13 +39,14 @@
"perf:test:results": "NODE_ENV=test flashlight report .perf/results.json",
"perf:measure": "NODE_ENV=test flashlight measure",
"intl:build": "yarn intl:extract && yarn intl:compile",
"intl:check": "yarn intl:extract && git diff-index -G'(^[^\\*# /])|(^#\\w)|(^\\s+[^\\*#/])' HEAD || (echo '\n⚠️ i18n detected un-extracted translations\n' && exit 1)",
"intl:extract": "lingui extract",
"intl:compile": "lingui compile",
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
"update-extensions": "bash scripts/updateExtensions.sh",
"export": "npx expo export",
"make-deploy-bundle": "bash scripts/bundleUpdate.sh"
"make-deploy-bundle": "bash scripts/bundleUpdate.sh",
"generate-webpack-stats-file": "EXPO_PUBLIC_GENERATE_STATS=1 yarn build-web",
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
},
"dependencies": {
"@atproto/api": "^0.12.2",
@@ -174,7 +176,7 @@
"react-native-safe-area-context": "4.8.2",
"react-native-screens": "~3.29.0",
"react-native-svg": "14.1.0",
"react-native-ui-text-view": "link:./modules/react-native-ui-text-view",
"react-native-uitextview": "^1.1.6",
"react-native-url-polyfill": "^1.3.0",
"react-native-uuid": "^2.0.1",
"react-native-version-number": "^0.3.6",
@@ -234,6 +236,7 @@
"babel-preset-expo": "^10.0.0",
"detox": "^20.14.8",
"eslint": "^8.19.0",
"eslint-plugin-bsky-internal": "link:./eslint",
"eslint-plugin-detox": "^1.0.0",
"eslint-plugin-ft-flow": "^2.0.3",
"eslint-plugin-lingui": "^0.2.0",
@@ -257,6 +260,7 @@
"typescript": "^5.3.3",
"url-loader": "^4.1.1",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.10.1",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
},
@@ -314,6 +318,9 @@
]
},
"lint-staged": {
"*{.js,.jsx,.ts,.tsx}": "yarn eslint --fix"
"*{.js,.jsx,.ts,.tsx}": [
"eslint --cache --fix",
"prettier --cache --write --ignore-unknown"
]
}
}
@@ -3594,3 +3594,19 @@ index 19b61ff..04a9951 100644
PasteInput_compileSdkVersion=30
PasteInput_buildToolsVersion=30.0.2
PasteInput_targetSdkVersion=30
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];
}
+26
View File
@@ -0,0 +1,26 @@
diff --git a/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift b/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift
index 189a5f5..8d5b8e6 100644
--- a/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift
+++ b/node_modules/expo-updates/ios/EXUpdates/Update/NewUpdate.swift
@@ -68,13 +68,20 @@ public final class NewUpdate: Update {
processedAssets.append(asset)
}
+ // Instead of relying on various hacks to get the correct format for the specific
+ // platform on the backend, we can just add this little patch..
+ let dateFormatter = DateFormatter()
+ dateFormatter.locale = Locale(identifier: "en_US_POSIX")
+ dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
+ let date = dateFormatter.date(from:commitTime) ?? RCTConvert.nsDate(commitTime)!
+
return Update(
manifest: manifest,
config: config,
database: database,
updateId: uuid,
scopeKey: config.scopeKey,
- commitTime: RCTConvert.nsDate(commitTime),
+ commitTime: date,
runtimeVersion: runtimeVersion,
keep: true,
status: UpdateStatus.StatusPending,
+7
View File
@@ -0,0 +1,7 @@
# Expo-Updates Patch
This is a small patch to convert timestamp formats that are returned from the backend. Instead of relying on the
backend to return the correct format for a specific format (the format required on Android is not the same as on iOS)
we can just add this conversion in.
Don't remove unless we make changes on the backend to support both platforms.
+5 -2
View File
@@ -9,10 +9,13 @@ rm -rf bundle.tar.gz
echo "Creating tarball..."
node scripts/bundleUpdate.js
cd bundleTempDir || exit
if [ -z "$RUNTIME_VERSION" ]; then
RUNTIME_VERSION=$(cat package.json | jq '.version' -r)
fi
cd bundleTempDir || exit
BUNDLE_VERSION=$(date +%s)
DEPLOYMENT_URL="https://updates.bsky.app/v1/upload?runtime-version=$RUNTIME_VERSION&bundle-version=$BUNDLE_VERSION"
DEPLOYMENT_URL="https://updates.bsky.app/v1/upload?runtime-version=$RUNTIME_VERSION&bundle-version=$BUNDLE_VERSION&channel=$CHANNEL_NAME&ios-build-number=$BSKY_IOS_BUILD_NUMBER&android-build-number=$BSKY_ANDROID_VERSION_CODE"
tar czvf bundle.tar.gz ./*
+2 -6
View File
@@ -1,11 +1,7 @@
#!/bin/bash
outputIos=$(eas build:version:get -p ios)
outputAndroid=$(eas build:version:get -p android)
currentIosVersion=${outputIos#*buildNumber - }
currentAndroidVersion=${outputAndroid#*versionCode - }
BSKY_IOS_BUILD_NUMBER=$((currentIosVersion+1))
BSKY_ANDROID_VERSION_CODE=$((currentAndroidVersion+1))
BSKY_IOS_BUILD_NUMBER=${outputIos#*buildNumber - }
BSKY_ANDROID_VERSION_CODE=${outputAndroid#*versionCode - }
bash -c "BSKY_IOS_BUILD_NUMBER=$BSKY_IOS_BUILD_NUMBER BSKY_ANDROID_VERSION_CODE=$BSKY_ANDROID_VERSION_CODE $*"
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
outputIos=$(eas build:version:get -p ios)
outputAndroid=$(eas build:version:get -p android)
currentIosVersion=${outputIos#*buildNumber - }
currentAndroidVersion=${outputAndroid#*versionCode - }
BSKY_IOS_BUILD_NUMBER=$((currentIosVersion+1))
BSKY_ANDROID_VERSION_CODE=$((currentAndroidVersion+1))
bash -c "BSKY_IOS_BUILD_NUMBER=$BSKY_IOS_BUILD_NUMBER BSKY_ANDROID_VERSION_CODE=$BSKY_ANDROID_VERSION_CODE $*"
+2
View File
@@ -19,6 +19,7 @@ import {init as initPersistedState} from '#/state/persisted'
import * as persisted from '#/state/persisted'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {useOTAUpdates} from 'lib/hooks/useOTAUpdates'
import {useNotificationsListener} from 'lib/notifications/notifications'
import {QueryProvider} from 'lib/react-query'
import {s} from 'lib/styles'
@@ -57,6 +58,7 @@ function InnerApp() {
const {_} = useLingui()
useIntentHandler()
useOTAUpdates()
// init
useEffect(() => {
+141
View File
@@ -0,0 +1,141 @@
import React, {useCallback} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useProfileQuery} from '#/state/queries/profile'
import {type SessionAccount, useSession} from '#/state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
import {Button} from './Button'
import {Text} from './Typography'
export function AccountList({
onSelectAccount,
onSelectOther,
otherLabel,
}: {
onSelectAccount: (account: SessionAccount) => void
onSelectOther: () => void
otherLabel?: string
}) {
const {isSwitchingAccounts, currentAccount, accounts} = useSession()
const t = useTheme()
const {_} = useLingui()
const onPressAddAccount = useCallback(() => {
onSelectOther()
}, [onSelectOther])
return (
<View
style={[
a.rounded_md,
a.overflow_hidden,
a.border,
t.atoms.border_contrast_low,
]}>
{accounts.map(account => (
<React.Fragment key={account.did}>
<AccountItem
account={account}
onSelect={onSelectAccount}
isCurrentAccount={account.did === currentAccount?.did}
/>
<View style={[a.border_b, t.atoms.border_contrast_low]} />
</React.Fragment>
))}
<Button
testID="chooseAddAccountBtn"
style={[a.flex_1]}
onPress={isSwitchingAccounts ? undefined : onPressAddAccount}
label={_(msg`Login to account that is not listed`)}>
{({hovered, pressed}) => (
<View
style={[
a.flex_1,
a.flex_row,
a.align_center,
{height: 48},
(hovered || pressed || isSwitchingAccounts) &&
t.atoms.bg_contrast_25,
]}>
<Text
style={[
a.align_baseline,
a.flex_1,
a.flex_row,
a.py_sm,
{paddingLeft: 48},
]}>
{otherLabel ?? <Trans>Other account</Trans>}
</Text>
<Chevron size="sm" style={[t.atoms.text, a.mr_md]} />
</View>
)}
</Button>
</View>
)
}
function AccountItem({
account,
onSelect,
isCurrentAccount,
}: {
account: SessionAccount
onSelect: (account: SessionAccount) => void
isCurrentAccount: boolean
}) {
const t = useTheme()
const {_} = useLingui()
const {data: profile} = useProfileQuery({did: account.did})
const onPress = React.useCallback(() => {
onSelect(account)
}, [account, onSelect])
return (
<Button
testID={`chooseAccountBtn-${account.handle}`}
key={account.did}
style={[a.flex_1]}
onPress={onPress}
label={
isCurrentAccount
? _(msg`Continue as ${account.handle} (currently signed in)`)
: _(msg`Sign in as ${account.handle}`)
}>
{({hovered, pressed}) => (
<View
style={[
a.flex_1,
a.flex_row,
a.align_center,
{height: 48},
(hovered || pressed) && t.atoms.bg_contrast_25,
]}>
<View style={a.p_md}>
<UserAvatar avatar={profile?.avatar} size={24} />
</View>
<Text style={[a.align_baseline, a.flex_1, a.flex_row, a.py_sm]}>
<Text style={[a.font_bold]}>
{profile?.displayName || account.handle}{' '}
</Text>
<Text style={[t.atoms.text_contrast_medium]}>{account.handle}</Text>
</Text>
{isCurrentAccount ? (
<Check
size="sm"
style={[{color: t.palette.positive_600}, a.mr_md]}
/>
) : (
<Chevron size="sm" style={[t.atoms.text, a.mr_md]} />
)}
</View>
)}
</Button>
)
}
+61
View File
@@ -0,0 +1,61 @@
# Button
`Button` is intended to be very flexible and can be used to create consistent
button styles, as seen in the Storybook, or can be used to create custom
buttons with totally independent styles.
## Usage
Typical usage looks like this, where the `variant`, `size`, `color`, and/or
`shape` are defined. Normal usage also should make use of the `ButtonText` and
`ButtonIcon` components so that they inherit styles from the top level props
like `color`.
```tsx
<Button
label='Typical button'
onPress={() => {}}
size='large'
variant='solid'
color='primary'
>
<ButtonIcon icon={Plus} position='left' />
<ButtonText>Hello world</ButtonText>
</Button>
```
Or:
```tsx
<Button
label='Typical button'
onPress={() => {}}
size='large'
variant='solid'
color='primary'
>
<ButtonText>Hello world</ButtonText>
<ButtonIcon icon={Plus} position='right' />
</Button>
```
Each of those top-level props props is optional in order to allow for "custom"
buttons, like this:
```tsx
<Button label='Custom button' onPress={() => {}}>
{({ hovered, focused, pressed, disabled }) => (
<View style={[
t.atoms.bg_contrast_25,
hovered && t.atoms.bg_contrast_50,
]}>
<Text>{disabled ? 'Disabled' : 'Click me!'}</Text>
</View>
))}
</Button>
```
In the custom button case, you have the _option_ to use `ButtonIcon` and
`ButtonText`, but without top-level props like `color`, they serve little
purpose, and it may be perferrable to define your own components for these
purposes.
@@ -1,19 +1,20 @@
import React from 'react'
import {
Pressable,
Text,
PressableProps,
TextProps,
ViewStyle,
AccessibilityProps,
View,
TextStyle,
StyleSheet,
Pressable,
PressableProps,
StyleProp,
StyleSheet,
Text,
TextProps,
TextStyle,
View,
ViewStyle,
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {Trans} from '@lingui/macro'
import {useTheme, atoms as a, tokens, android, flatten} from '#/alf'
import {android, atoms as a, flatten, tokens, useTheme} from '#/alf'
import {Props as SVGIconProps} from '#/components/icons/common'
import {normalizeTextStyles} from '#/components/Typography'
@@ -403,7 +404,9 @@ export function Button({
</View>
)}
<Context.Provider value={context}>
{typeof children === 'string' ? (
{/* @ts-ignore */}
{typeof children === 'string' || children?.type === Trans ? (
/* @ts-ignore */
<ButtonText>{children}</ButtonText>
) : typeof children === 'function' ? (
children(context)
+16 -9
View File
@@ -1,13 +1,15 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/core'
import {StackActions} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {Text} from '#/components/Typography'
import {View} from 'react-native'
import {Button} from '#/components/Button'
import {useNavigation} from '@react-navigation/core'
import {NavigationProp} from 'lib/routes/types'
import {StackActions} from '@react-navigation/native'
import {router} from '#/routes'
export function Error({
@@ -20,6 +22,7 @@ export function Error({
onRetry?: () => unknown
}) {
const navigation = useNavigation<NavigationProp>()
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
@@ -68,21 +71,25 @@ export function Error({
<Button
variant="solid"
color="primary"
label="Click here"
label={_(msg`Press to retry`)}
onPress={onRetry}
size="large"
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
Retry
<ButtonText>
<Trans>Retry</Trans>
</ButtonText>
</Button>
)}
<Button
variant="solid"
color={onRetry ? 'secondary' : 'primary'}
label="Click here"
label={_(msg`Return to previous page`)}
onPress={onGoBack}
size="large"
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
Go Back
<ButtonText>
<Trans>Go Back</Trans>
</ButtonText>
</Button>
</View>
</CenteredView>
+47 -49
View File
@@ -1,47 +1,54 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {List} from '#/view/com/util/List'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {useLikedByQuery} from '#/state/queries/post-liked-by'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {ListFooter} from '#/components/Lists'
import {cleanError} from 'lib/strings/errors'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {List} from '#/view/com/util/List'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {atoms as a, useTheme} from '#/alf'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
function renderItem({item}: {item: GetLikes.Like}) {
return <ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
}
function keyExtractor(item: GetLikes.Like) {
return item.actor.did
}
export function LikedByList({uri}: {uri: string}) {
const t = useTheme()
const {_} = useLingui()
const initialNumToRender = useInitialNumToRender()
const [isPTRing, setIsPTRing] = React.useState(false)
const {
data: resolvedUri,
error: resolveError,
isFetching: isFetchingResolvedUri,
isLoading: isUriLoading,
} = useResolveUriQuery(uri)
const {
data,
isFetching,
isFetched,
isRefetching,
isLoading: isLikedByLoading,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
isError,
error: likedByError,
refetch,
} = useLikedByQuery(resolvedUri?.uri)
const error = resolveError || likedByError
const isError = !!resolveError || !!likedByError
const likes = React.useMemo(() => {
if (data?.pages) {
return data.pages.flatMap(page => page.likes)
}
return []
}, [data])
const initialNumToRender = useInitialNumToRender()
const error = resolveError || likedByError
const onRefresh = React.useCallback(async () => {
setIsPTRing(true)
@@ -54,56 +61,47 @@ export function LikedByList({uri}: {uri: string}) {
}, [refetch, setIsPTRing])
const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
if (isFetchingNextPage || !hasNextPage || isError) return
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more likes', {message: err})
}
}, [isFetching, hasNextPage, isError, fetchNextPage])
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
const renderItem = React.useCallback(({item}: {item: GetLikes.Like}) => {
if (likes.length < 1) {
return (
<ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
)
}, [])
if (isFetchingResolvedUri || !isFetched) {
return (
<View style={[a.w_full, a.align_center, a.p_lg]}>
<Loader size="xl" />
</View>
<ListMaybePlaceholder
isLoading={isUriLoading || isLikedByLoading}
isError={isError}
emptyType="results"
emptyMessage={_(
msg`Nobody has liked this yet. Maybe you should be the first!`,
)}
errorMessage={cleanError(resolveError || error)}
onRetry={isError ? refetch : undefined}
/>
)
}
return likes.length ? (
return (
<List
data={likes}
keyExtractor={item => item.actor.did}
renderItem={renderItem}
keyExtractor={keyExtractor}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={3}
renderItem={renderItem}
initialNumToRender={initialNumToRender}
ListFooterComponent={() => (
ListFooterComponent={
<ListFooter
isFetching={isFetching && !isRefetching}
isError={isError}
error={error ? error.toString() : undefined}
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
/>
)}
}
onEndReachedThreshold={3}
initialNumToRender={initialNumToRender}
windowSize={11}
/>
) : (
<View style={[a.p_lg]}>
<View style={[a.p_lg, a.rounded_sm, t.atoms.bg_contrast_25]}>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
Nobody has liked this yet. Maybe you should be the first!
</Trans>
</Text>
</View>
</View>
)
}
+43 -9
View File
@@ -1,23 +1,24 @@
import React from 'react'
import {GestureResponderEvent} from 'react-native'
import {useLinkProps, StackActions} from '@react-navigation/native'
import {sanitizeUrl} from '@braintree/sanitize-url'
import {StackActions, useLinkProps} from '@react-navigation/native'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {isWeb} from '#/platform/detection'
import {useTheme, web, flatten, TextStyleProp, atoms as a} from '#/alf'
import {Button, ButtonProps} from '#/components/Button'
import {AllNavigatorParams} from '#/lib/routes/types'
import {shareUrl} from '#/lib/sharing'
import {
convertBskyAppUrlIfNeeded,
isExternalUrl,
linkRequiresWarning,
} from '#/lib/strings/url-helpers'
import {isNative, isWeb} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {router} from '#/routes'
import {Text, TextProps} from '#/components/Typography'
import {useOpenLink} from 'state/preferences/in-app-browser'
import {useOpenLink} from '#/state/preferences/in-app-browser'
import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf'
import {Button, ButtonProps} from '#/components/Button'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Text, TextProps} from '#/components/Typography'
import {router} from '#/routes'
/**
* Only available within a `Link`, since that inherits from `Button`.
@@ -60,6 +61,11 @@ type BaseLinkProps = Pick<
* Web-only attribute. Sets `download` attr on web.
*/
download?: string
/**
* Native-only attribute. If true, will open the share sheet on long press.
*/
shareOnLongPress?: boolean
}
export function useLink({
@@ -68,6 +74,7 @@ export function useLink({
action = 'push',
disableMismatchWarning,
onPress: outerOnPress,
shareOnLongPress,
}: BaseLinkProps & {
displayText: string
}) {
@@ -157,10 +164,34 @@ export function useLink({
],
)
const handleLongPress = React.useCallback(() => {
const requiresWarning = Boolean(
!disableMismatchWarning &&
displayText &&
isExternal &&
linkRequiresWarning(href, displayText),
)
if (requiresWarning) {
openModal({
name: 'link-warning',
text: displayText,
href: href,
share: true,
})
} else {
shareUrl(href)
}
}, [disableMismatchWarning, displayText, href, isExternal, openModal])
const onLongPress =
isNative && isExternal && shareOnLongPress ? handleLongPress : undefined
return {
isExternal,
href,
onPress,
onLongPress,
}
}
@@ -229,16 +260,18 @@ export function InlineLink({
download,
selectable,
label,
shareOnLongPress,
...rest
}: InlineLinkProps) {
const t = useTheme()
const stringChildren = typeof children === 'string'
const {href, isExternal, onPress} = useLink({
const {href, isExternal, onPress, onLongPress} = useLink({
to,
displayText: stringChildren ? children : '',
action,
disableMismatchWarning,
onPress: outerOnPress,
shareOnLongPress,
})
const {
state: hovered,
@@ -270,6 +303,7 @@ export function InlineLink({
]}
role="link"
onPress={download ? undefined : onPress}
onLongPress={onLongPress}
onPressIn={onPressIn}
onPressOut={onPressOut}
onFocus={onFocus}
+25 -31
View File
@@ -1,25 +1,23 @@
import React from 'react'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {CenteredView} from 'view/com/util/Views'
import {Loader} from '#/components/Loader'
import {cleanError} from 'lib/strings/errors'
import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {Text} from '#/components/Typography'
import {Error} from '#/components/Error'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
export function ListFooter({
isFetching,
isError,
isFetchingNextPage,
error,
onRetry,
height,
}: {
isFetching?: boolean
isError?: boolean
isFetchingNextPage?: boolean
error?: string
onRetry?: () => Promise<unknown>
height?: number
@@ -36,32 +34,26 @@ export function ListFooter({
t.atoms.border_contrast_low,
{height: height ?? 180, paddingTop: 30},
]}>
{isFetching ? (
{isFetchingNextPage ? (
<Loader size="xl" />
) : (
<ListFooterMaybeError
isError={isError}
error={error}
onRetry={onRetry}
/>
<ListFooterMaybeError error={error} onRetry={onRetry} />
)}
</View>
)
}
function ListFooterMaybeError({
isError,
error,
onRetry,
}: {
isError?: boolean
error?: string
onRetry?: () => Promise<unknown>
}) {
const t = useTheme()
const {_} = useLingui()
if (!isError) return null
if (!error) return null
return (
<View style={[a.w_full, a.px_lg]}>
@@ -128,7 +120,7 @@ export function ListHeaderDesktop({
export function ListMaybePlaceholder({
isLoading,
isEmpty,
noEmpty,
isError,
emptyTitle,
emptyMessage,
@@ -138,7 +130,7 @@ export function ListMaybePlaceholder({
onRetry,
}: {
isLoading: boolean
isEmpty?: boolean
noEmpty?: boolean
isError?: boolean
emptyTitle?: string
emptyMessage?: string
@@ -151,16 +143,6 @@ export function ListMaybePlaceholder({
const {_} = useLingui()
const {gtMobile, gtTablet} = useBreakpoints()
if (!isLoading && isError) {
return (
<Error
title={errorTitle ?? _(msg`Oops!`)}
message={errorMessage ?? _(`Something went wrong!`)}
onRetry={onRetry}
/>
)
}
if (isLoading) {
return (
<CenteredView
@@ -180,7 +162,17 @@ export function ListMaybePlaceholder({
)
}
if (isEmpty) {
if (isError) {
return (
<Error
title={errorTitle ?? _(msg`Oops!`)}
message={errorMessage ?? _(`Something went wrong!`)}
onRetry={onRetry}
/>
)
}
if (!noEmpty) {
return (
<Error
title={
@@ -197,4 +189,6 @@ export function ListMaybePlaceholder({
/>
)
}
return null
}
+3 -4
View File
@@ -3,11 +3,10 @@ import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonColor, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Text} from '#/components/Typography'
export {useDialogControl as usePromptControl} from '#/components/Dialog'
@@ -80,7 +79,7 @@ export function Actions({children}: React.PropsWithChildren<{}>) {
<View
style={[
a.w_full,
a.gap_sm,
a.gap_md,
a.justify_end,
gtMobile
? [a.flex_row, a.flex_row_reverse, a.justify_start]
@@ -1,18 +1,16 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyLabelerDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppBskyLabelerDefs} from '@atproto/api'
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, useButtonContext} from '#/components/Button'
import {Divider} from '#/components/Divider'
import * as LabelingServiceCard from '#/components/LabelingServiceCard'
import {Text} from '#/components/Typography'
import {ReportDialogProps} from './types'
export function SelectLabelerView({
@@ -1,16 +1,15 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyLabelerDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppBskyLabelerDefs} from '@atproto/api'
import {useReportOptions, ReportOption} from '#/lib/moderation/useReportOptions'
import {DMCA_LINK} from '#/components/ReportDialog/const'
import {ReportOption, useReportOptions} from '#/lib/moderation/useReportOptions'
import {Link} from '#/components/Link'
import {DMCA_LINK} from '#/components/ReportDialog/const'
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {
Button,
ButtonIcon,
@@ -19,11 +18,11 @@ import {
} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {
ChevronRight_Stroke2_Corner0_Rounded as ChevronRight,
ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft,
ChevronRight_Stroke2_Corner0_Rounded as ChevronRight,
} from '#/components/icons/Chevron'
import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from '#/components/icons/SquareArrowTopRight'
import {Text} from '#/components/Typography'
import {ReportDialogProps} from './types'
export function SelectReportOptionView({
+9 -8
View File
@@ -1,15 +1,15 @@
import React from 'react'
import {RichText as RichTextAPI, AppBskyRichtextFacet} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, TextStyleProp, flatten, useTheme, web, native} from '#/alf'
import {InlineLink} from '#/components/Link'
import {Text, TextProps} from '#/components/Typography'
import {toShortUrl} from 'lib/strings/url-helpers'
import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
import {toShortUrl} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {InlineLink} from '#/components/Link'
import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
import {Text, TextProps} from '#/components/Typography'
const WORD_WRAP = {wordWrap: 1}
@@ -105,7 +105,8 @@ export function RichText({
to={link.uri}
style={[...styles, {pointerEvents: 'auto'}]}
// @ts-ignore TODO
dataSet={WORD_WRAP}>
dataSet={WORD_WRAP}
shareOnLongPress>
{toShortUrl(segment.text)}
</InlineLink>,
)
+6 -14
View File
@@ -1,14 +1,9 @@
import React from 'react'
import {
Text as RNText,
StyleProp,
TextStyle,
TextProps as RNTextProps,
} from 'react-native'
import {UITextView} from 'react-native-ui-text-view'
import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
import {UITextView} from 'react-native-uitextview'
import {useTheme, atoms, web, flatten} from '#/alf'
import {isIOS, isNative} from '#/platform/detection'
import {isNative} from '#/platform/detection'
import {atoms, flatten, useTheme, web} from '#/alf'
export type TextProps = RNTextProps & {
/**
@@ -61,11 +56,8 @@ export function normalizeTextStyles(styles: StyleProp<TextStyle>) {
export function Text({style, selectable, ...rest}: TextProps) {
const t = useTheme()
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)])
return selectable && isIOS ? (
<UITextView style={s} {...rest} />
) : (
<RNText selectable={selectable} style={s} {...rest} />
)
return <UITextView selectable={selectable} uiTextView style={s} {...rest} />
}
export function createHeadingElement({level}: {level: number}) {
+10 -10
View File
@@ -1,23 +1,23 @@
import React from 'react'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as Dialog from '#/components/Dialog'
import {Text} from '../Typography'
import {DateInput} from '#/view/com/util/forms/DateInput'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {isIOS, isWeb} from '#/platform/detection'
import {
usePreferencesQuery,
usePreferencesSetBirthDateMutation,
UsePreferencesQueryResponse,
usePreferencesSetBirthDateMutation,
} from '#/state/queries/preferences'
import {Button, ButtonIcon, ButtonText} from '../Button'
import {atoms as a, useTheme} from '#/alf'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {cleanError} from '#/lib/strings/errors'
import {isIOS, isWeb} from '#/platform/detection'
import {DateInput} from '#/view/com/util/forms/DateInput'
import {atoms as a, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {Loader} from '#/components/Loader'
import {Button, ButtonIcon, ButtonText} from '../Button'
import {Text} from '../Typography'
export function BirthDateSettingsDialog({
control,
+61
View File
@@ -0,0 +1,61 @@
import React, {useCallback} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {type SessionAccount, useSession} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
import {atoms as a} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {AccountList} from '../AccountList'
import {Text} from '../Typography'
export function SwitchAccountDialog({
control,
}: {
control: Dialog.DialogControlProps
}) {
const {_} = useLingui()
const {currentAccount} = useSession()
const {onPressSwitchAccount} = useAccountSwitcher()
const {setShowLoggedOut} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements()
const onSelectAccount = useCallback(
(account: SessionAccount) => {
if (account.did === currentAccount?.did) {
control.close()
} else {
onPressSwitchAccount(account, 'SwitchAccount')
}
},
[currentAccount, control, onPressSwitchAccount],
)
const onPressAddAccount = useCallback(() => {
setShowLoggedOut(true)
closeAllActiveElements()
}, [setShowLoggedOut, closeAllActiveElements])
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner label={_(msg`Switch Account`)}>
<View style={[a.gap_lg]}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>Switch Account</Trans>
</Text>
<AccountList
onSelectAccount={onSelectAccount}
onSelectOther={onPressAddAccount}
otherLabel={_(msg`Add account`)}
/>
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
@@ -1,22 +1,21 @@
import React from 'react'
import {View} from 'react-native'
import {InterpretedLabelValueDefinition, LabelPreference} from '@atproto/api'
import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
import {useLabelBehaviorDescription} from '#/lib/moderation/useLabelBehaviorDescription'
import {getLabelStrings} from '#/lib/moderation/useLabelInfo'
import {
usePreferencesQuery,
usePreferencesSetContentLabelMutation,
} from '#/state/queries/preferences'
import {useLabelBehaviorDescription} from '#/lib/moderation/useLabelBehaviorDescription'
import {getLabelStrings} from '#/lib/moderation/useLabelInfo'
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import {InlineLink} from '#/components/Link'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {InlineLink} from '#/components/Link'
import {Text} from '#/components/Typography'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo'
export function Outer({children}: React.PropsWithChildren<{}>) {
return (
+15 -8
View File
@@ -1,27 +1,26 @@
import React from 'react'
import {
TouchableWithoutFeedback,
StyleProp,
TouchableWithoutFeedback,
View,
ViewStyle,
} from 'react-native'
import {useNavigation} from '@react-navigation/native'
import {ModerationUI} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {NavigationProp} from 'lib/routes/types'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {useTheme, atoms as a} from '#/alf'
import {CenteredView} from '#/view/com/util/Views'
import {Text} from '#/components/Typography'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {
ModerationDetailsDialog,
useModerationDetailsDialogControl,
} from '#/components/moderation/ModerationDetailsDialog'
import {Text} from '#/components/Typography'
export function ScreenHider({
testID,
@@ -125,7 +124,15 @@ export function ScreenHider({
accessibilityRole="button"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint="">
<Text style={[a.text_lg, {color: t.palette.primary_500}]}>
<Text
style={[
a.text_lg,
{
color: t.palette.primary_500,
// @ts-ignore web only -prf
cursor: 'pointer',
},
]}>
<Trans>Learn More</Trans>
</Text>
</TouchableWithoutFeedback>
+7 -3
View File
@@ -1,5 +1,9 @@
import VersionNumber from 'react-native-version-number'
import * as Updates from 'expo-updates'
export const updateChannel = Updates.channel
export const appVersion = `${VersionNumber.appVersion} (${VersionNumber.buildVersion})`
export const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
export const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
const UPDATES_CHANNEL = IS_TESTFLIGHT ? 'testflight' : 'production'
export const appVersion = `${VersionNumber.appVersion} (${
VersionNumber.buildVersion
}, ${IS_DEV ? 'development' : UPDATES_CHANNEL})`
+142
View File
@@ -0,0 +1,142 @@
import React from 'react'
import {Alert, AppState, AppStateStatus} from 'react-native'
import app from 'react-native-version-number'
import {
checkForUpdateAsync,
fetchUpdateAsync,
isEnabled,
reloadAsync,
setExtraParamAsync,
useUpdates,
} from 'expo-updates'
import {logger} from '#/logger'
import {IS_TESTFLIGHT} from 'lib/app-info'
import {isIOS} from 'platform/detection'
const MINIMUM_MINIMIZE_TIME = 15 * 60e3
async function setExtraParams() {
await setExtraParamAsync(
isIOS ? 'ios-build-number' : 'android-build-number',
// Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is.
// This just ensures it gets passed as a string
`${app.buildVersion}`,
)
await setExtraParamAsync(
'channel',
IS_TESTFLIGHT ? 'testflight' : 'production',
)
}
export function useOTAUpdates() {
const appState = React.useRef<AppStateStatus>('active')
const lastMinimize = React.useRef(0)
const ranInitialCheck = React.useRef(false)
const timeout = React.useRef<NodeJS.Timeout>()
const {isUpdatePending} = useUpdates()
const setCheckTimeout = React.useCallback(() => {
timeout.current = setTimeout(async () => {
try {
await setExtraParams()
logger.debug('Checking for update...')
const res = await checkForUpdateAsync()
if (res.isAvailable) {
logger.debug('Attempting to fetch update...')
await fetchUpdateAsync()
} else {
logger.debug('No update available.')
}
} catch (e) {
logger.warn('OTA Update Error', {error: `${e}`})
}
}, 10e3)
}, [])
const onIsTestFlight = React.useCallback(() => {
setTimeout(async () => {
try {
await setExtraParams()
const res = await checkForUpdateAsync()
if (res.isAvailable) {
await fetchUpdateAsync()
Alert.alert(
'Update Available',
'A new version of the app is available. Relaunch now?',
[
{
text: 'No',
style: 'cancel',
},
{
text: 'Relaunch',
style: 'default',
onPress: async () => {
await reloadAsync()
},
},
],
)
}
} catch (e: any) {
// No need to handle
}
}, 3e3)
}, [])
React.useEffect(() => {
// For Testflight users, we can prompt the user to update immediately whenever there's an available update. This
// is suspect however with the Apple App Store guidelines, so we don't want to prompt production users to update
// immediately.
if (IS_TESTFLIGHT) {
onIsTestFlight()
return
} else if (!isEnabled || __DEV__ || ranInitialCheck.current) {
// Development client shouldn't check for updates at all, so we skip that here.
return
}
setCheckTimeout()
ranInitialCheck.current = true
}, [onIsTestFlight, setCheckTimeout])
// After the app has been minimized for 30 minutes, we want to either A. install an update if one has become available
// or B check for an update again.
React.useEffect(() => {
if (!isEnabled) return
const subscription = AppState.addEventListener(
'change',
async nextAppState => {
if (
appState.current.match(/inactive|background/) &&
nextAppState === 'active'
) {
// If it's been 15 minutes since the last "minimize", we should feel comfortable updating the client since
// chances are that there isn't anything important going on in the current session.
if (lastMinimize.current <= Date.now() - MINIMUM_MINIMIZE_TIME) {
if (isUpdatePending) {
await reloadAsync()
} else {
setCheckTimeout()
}
}
} else {
lastMinimize.current = Date.now()
}
appState.current = nextAppState
},
)
return () => {
clearTimeout(timeout.current)
subscription.remove()
}
}, [isUpdatePending, setCheckTimeout])
}
+8 -8
View File
@@ -1,6 +1,6 @@
import React from 'react'
import {StyleProp, TextStyle, ViewStyle} from 'react-native'
import Svg, {Path, Rect, Line, Ellipse} from 'react-native-svg'
import Svg, {Ellipse, Line, Path, Rect} from 'react-native-svg'
export function GridIcon({
style,
@@ -141,8 +141,8 @@ export function MagnifyingGlassIcon2({
width={size || 24}
height={size || 24}
style={style}>
<Ellipse cx="12" cy="11" rx="9" ry="9" />
<Line x1="19" y1="17.3" x2="23.5" y2="21" strokeLinecap="round" />
<Ellipse cx="12" cy="10.5" rx="9" ry="9" />
<Line x1="18.5" y1="17" x2="22" y2="20.5" strokeLinecap="round" />
</Svg>
)
}
@@ -167,14 +167,14 @@ export function MagnifyingGlassIcon2Solid({
style={style}>
<Ellipse
cx="12"
cy="11"
rx="7"
ry="7"
cy="10.5"
rx="6.5"
ry="6.5"
stroke="none"
fill="currentColor"
/>
<Ellipse cx="12" cy="11" rx="9" ry="9" />
<Line x1="19" y1="17.3" x2="23.5" y2="21" strokeLinecap="round" />
<Ellipse cx="12" cy="10.5" rx="9" ry="9" />
<Line x1="18.5" y1="17" x2="22" y2="20.5" strokeLinecap="round" />
</Svg>
)
}
+2 -2
View File
@@ -1,6 +1,6 @@
import {useMemo} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useMemo} from 'react'
export type GlobalLabelStrings = Record<
string,
@@ -31,7 +31,7 @@ export function useGlobalLabelStrings(): GlobalLabelStrings {
),
},
porn: {
name: _(msg`Pornography`),
name: _(msg`Adult Content`),
description: _(msg`Explicit sexual images.`),
},
sexual: {
+3 -3
View File
@@ -1,7 +1,7 @@
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useMemo} from 'react'
import {ComAtprotoModerationDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export interface ReportOption {
reason: string
@@ -68,7 +68,7 @@ export function useReportOptions(): ReportOptions {
{
reason: ComAtprotoModerationDefs.REASONSEXUAL,
title: _(msg`Unwanted Sexual Content`),
description: _(msg`Nudity or pornography not labeled as such`),
description: _(msg`Nudity or adult content not labeled as such`),
},
...common,
],
+3 -3
View File
@@ -11,7 +11,8 @@ import {
import {isNative} from '#/platform/detection'
// any query keys in this array will be persisted to AsyncStorage
const STORED_CACHE_QUERY_KEYS = ['labelers-detailed-info']
export const labelersDetailedInfoQueryKeyRoot = 'labelers-detailed-info'
const STORED_CACHE_QUERY_KEY_ROOTS = [labelersDetailedInfoQueryKeyRoot]
focusManager.setEventListener(onFocus => {
if (isNative) {
@@ -65,7 +66,7 @@ const dehydrateOptions: PersistQueryClientProviderProps['persistOptions']['dehyd
{
shouldDehydrateMutation: (_: any) => false,
shouldDehydrateQuery: query => {
return STORED_CACHE_QUERY_KEYS.includes(String(query.queryKey[0]))
return STORED_CACHE_QUERY_KEY_ROOTS.includes(String(query.queryKey[0]))
},
}
@@ -113,7 +114,6 @@ function QueryProviderInner({
dehydrateOptions,
}
})
console.log('QUERY', currentDid)
return (
<PersistQueryClientProvider
client={queryClient}
+4 -3
View File
@@ -1,8 +1,9 @@
import {isIOS, isAndroid} from 'platform/detection'
import {Share} from 'react-native'
// import * as Sharing from 'expo-sharing'
import Clipboard from '@react-native-clipboard/clipboard'
import * as Toast from '../view/com/util/Toast'
import {Share} from 'react-native'
import {isAndroid, isIOS} from 'platform/detection'
import * as Toast from '#/view/com/util/Toast'
/**
* This function shares a URL using the native Share API if available, or copies it to the clipboard
+2 -4
View File
@@ -8,6 +8,7 @@ import {
useGate as useStatsigGate,
} from 'statsig-react-native-expo'
import {logger} from '#/logger'
import {useSession} from '../../state/session'
import {LogEvents} from './events'
@@ -56,10 +57,7 @@ export function logEvent<E extends keyof LogEvents>(
}
} catch (e) {
// A log should never interrupt the calling code, whatever happens.
// Rethrow on a clean stack.
setTimeout(() => {
throw e
})
logger.error('Failed to log an event', {message: e})
}
}
+13 -6
View File
@@ -1,7 +1,8 @@
import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
import lande from 'lande'
import {hasProp} from 'lib/type-guards'
import * as bcp47Match from 'bcp-47-match'
import lande from 'lande'
import {hasProp} from 'lib/type-guards'
import {
AppLanguage,
LANGUAGES_MAP_CODE2,
@@ -118,6 +119,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
switch (lang) {
case 'en':
return AppLanguage.en
case 'ca':
return AppLanguage.ca
case 'de':
return AppLanguage.de
case 'es':
@@ -126,24 +129,28 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
return AppLanguage.fi
case 'fr':
return AppLanguage.fr
case 'ga':
return AppLanguage.ga
case 'hi':
return AppLanguage.hi
case 'id':
return AppLanguage.id
case 'it':
return AppLanguage.it
case 'ja':
return AppLanguage.ja
case 'ko':
return AppLanguage.ko
case 'pt-BR':
return AppLanguage.pt_BR
case 'tr':
return AppLanguage.tr
case 'uk':
return AppLanguage.uk
case 'ca':
return AppLanguage.ca
case 'zh-CN':
return AppLanguage.zh_CN
case 'it':
return AppLanguage.it
case 'zh-TW':
return AppLanguage.zh_TW
default:
continue
}
+28 -14
View File
@@ -1,30 +1,36 @@
import {useEffect} from 'react'
import {i18n} from '@lingui/core'
import {useLanguagePrefs} from '#/state/preferences'
import {messages as messagesEn} from '#/locale/locales/en/messages'
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
import {AppLanguage} from '#/locale/languages'
import {messages as messagesCa} from '#/locale/locales/ca/messages'
import {messages as messagesDe} from '#/locale/locales/de/messages'
import {messages as messagesId} from '#/locale/locales/id/messages'
import {messages as messagesEn} from '#/locale/locales/en/messages'
import {messages as messagesEs} from '#/locale/locales/es/messages'
import {messages as messagesFi} from '#/locale/locales/fi/messages'
import {messages as messagesFr} from '#/locale/locales/fr/messages'
import {messages as messagesGa} from '#/locale/locales/ga/messages'
import {messages as messagesHi} from '#/locale/locales/hi/messages'
import {messages as messagesId} from '#/locale/locales/id/messages'
import {messages as messagesIt} from '#/locale/locales/it/messages'
import {messages as messagesJa} from '#/locale/locales/ja/messages'
import {messages as messagesKo} from '#/locale/locales/ko/messages'
import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages'
import {messages as messagesTr} from '#/locale/locales/tr/messages'
import {messages as messagesUk} from '#/locale/locales/uk/messages'
import {messages as messagesCa} from '#/locale/locales/ca/messages'
import {messages as messagesZh_CN} from '#/locale/locales/zh-CN/messages'
import {messages as messagesIt} from '#/locale/locales/it/messages'
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
import {AppLanguage} from '#/locale/languages'
import {messages as messagesZh_TW} from '#/locale/locales/zh-TW/messages'
import {useLanguagePrefs} from '#/state/preferences'
/**
* We do a dynamic import of just the catalog that we need
*/
export async function dynamicActivate(locale: AppLanguage) {
switch (locale) {
case AppLanguage.ca: {
i18n.loadAndActivate({locale, messages: messagesCa})
break
}
case AppLanguage.de: {
i18n.loadAndActivate({locale, messages: messagesDe})
break
@@ -41,6 +47,10 @@ export async function dynamicActivate(locale: AppLanguage) {
i18n.loadAndActivate({locale, messages: messagesFr})
break
}
case AppLanguage.ga: {
i18n.loadAndActivate({locale, messages: messagesGa})
break
}
case AppLanguage.hi: {
i18n.loadAndActivate({locale, messages: messagesHi})
break
@@ -49,6 +59,10 @@ export async function dynamicActivate(locale: AppLanguage) {
i18n.loadAndActivate({locale, messages: messagesId})
break
}
case AppLanguage.it: {
i18n.loadAndActivate({locale, messages: messagesIt})
break
}
case AppLanguage.ja: {
i18n.loadAndActivate({locale, messages: messagesJa})
break
@@ -61,20 +75,20 @@ export async function dynamicActivate(locale: AppLanguage) {
i18n.loadAndActivate({locale, messages: messagesPt_BR})
break
}
case AppLanguage.uk: {
i18n.loadAndActivate({locale, messages: messagesUk})
case AppLanguage.tr: {
i18n.loadAndActivate({locale, messages: messagesTr})
break
}
case AppLanguage.ca: {
i18n.loadAndActivate({locale, messages: messagesCa})
case AppLanguage.uk: {
i18n.loadAndActivate({locale, messages: messagesUk})
break
}
case AppLanguage.zh_CN: {
i18n.loadAndActivate({locale, messages: messagesZh_CN})
break
}
case AppLanguage.it: {
i18n.loadAndActivate({locale, messages: messagesIt})
case AppLanguage.zh_TW: {
i18n.loadAndActivate({locale, messages: messagesZh_TW})
break
}
default: {
+19 -7
View File
@@ -1,9 +1,9 @@
import {useEffect} from 'react'
import {i18n} from '@lingui/core'
import {useLanguagePrefs} from '#/state/preferences'
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
import {AppLanguage} from '#/locale/languages'
import {useLanguagePrefs} from '#/state/preferences'
/**
* We do a dynamic import of just the catalog that we need
@@ -12,6 +12,10 @@ export async function dynamicActivate(locale: AppLanguage) {
let mod: any
switch (locale) {
case AppLanguage.ca: {
mod = await import(`./locales/ca/messages`)
break
}
case AppLanguage.de: {
mod = await import(`./locales/de/messages`)
break
@@ -28,6 +32,10 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/fr/messages`)
break
}
case AppLanguage.ga: {
mod = await import(`./locales/ga/messages`)
break
}
case AppLanguage.hi: {
mod = await import(`./locales/hi/messages`)
break
@@ -36,6 +44,10 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/id/messages`)
break
}
case AppLanguage.it: {
mod = await import(`./locales/it/messages`)
break
}
case AppLanguage.ja: {
mod = await import(`./locales/ja/messages`)
break
@@ -48,20 +60,20 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/pt-BR/messages`)
break
}
case AppLanguage.uk: {
mod = await import(`./locales/uk/messages`)
case AppLanguage.tr: {
mod = await import(`./locales/tr/messages`)
break
}
case AppLanguage.ca: {
mod = await import(`./locales/ca/messages`)
case AppLanguage.uk: {
mod = await import(`./locales/uk/messages`)
break
}
case AppLanguage.zh_CN: {
mod = await import(`./locales/zh-CN/messages`)
break
}
case AppLanguage.it: {
mod = await import(`./locales/it/messages`)
case AppLanguage.zh_TW: {
mod = await import(`./locales/zh-TW/messages`)
break
}
default: {
+11 -5
View File
@@ -6,19 +6,22 @@ interface Language {
export enum AppLanguage {
en = 'en',
ca = 'ca',
de = 'de',
es = 'es',
fi = 'fi',
fr = 'fr',
ga = 'ga',
hi = 'hi',
id = 'id',
it = 'it',
ja = 'ja',
ko = 'ko',
pt_BR = 'pt-BR',
tr = 'tr',
uk = 'uk',
ca = 'ca',
zh_CN = 'zh-CN',
it = 'it',
zh_TW = 'zh-TW',
}
interface AppLanguageConfig {
@@ -28,19 +31,22 @@ interface AppLanguageConfig {
export const APP_LANGUAGES: AppLanguageConfig[] = [
{code2: AppLanguage.en, name: 'English'},
{code2: AppLanguage.ca, name: 'Català Catalan'},
{code2: AppLanguage.de, name: 'Deutsch German'},
{code2: AppLanguage.es, name: 'Español Spanish'},
{code2: AppLanguage.fi, name: 'Suomi Finnish'},
{code2: AppLanguage.fr, name: 'Français French'},
{code2: AppLanguage.ga, name: 'Gaeilge Irish'},
{code2: AppLanguage.hi, name: 'हिंदी Hindi'},
{code2: AppLanguage.id, name: 'Bahasa Indonesia Indonesian'},
{code2: AppLanguage.it, name: 'Italiano Italian'},
{code2: AppLanguage.ja, name: '日本語 Japanese'},
{code2: AppLanguage.ko, name: '한국어 Korean'},
{code2: AppLanguage.pt_BR, name: 'Português (BR) Portuguese (BR)'},
{code2: AppLanguage.tr, name: 'Türkçe Turkish'},
{code2: AppLanguage.uk, name: 'Українська Ukrainian'},
{code2: AppLanguage.ca, name: 'Català Catalan'},
{code2: AppLanguage.zh_CN, name: '简体中文(中国) Chinese (Simplified)'},
{code2: AppLanguage.it, name: 'Italiano - Italian'},
{code2: AppLanguage.zh_CN, name: '简体中文(中国)– Chinese (Simplified)'},
{code2: AppLanguage.zh_TW, name: '繁體中文(臺灣) Chinese (Traditional)'},
]
export const LANGUAGES: Language[] = [
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+21
View File
@@ -6,3 +6,24 @@ findLast.shim()
// @ts-ignore whatever typescript wants to complain about here, I dont care about -prf
window.setImmediate = (cb: () => void) => setTimeout(cb, 0)
if (process.env.NODE_ENV !== 'production') {
// In development, react-native-web's <View> tries to validate that
// text is wrapped into <Text>. It doesn't catch all cases but is useful.
// Unfortunately, it only does that via console.error so it's easy to miss.
// This is a hack to get it showing as a redbox on the web so we catch it early.
const realConsoleError = console.error
const thrownErrors = new WeakSet()
console.error = function consoleErrorWrapper(msgOrError) {
if (
typeof msgOrError === 'string' &&
msgOrError.startsWith('Unexpected text node')
) {
const err = new Error(msgOrError)
thrownErrors.add(err)
throw err
} else if (!thrownErrors.has(msgOrError)) {
return realConsoleError.apply(this, arguments as any)
}
}
}
+33 -33
View File
@@ -1,28 +1,30 @@
import React from 'react'
import {ListRenderItemInfo, Pressable} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {useSetMinimalShellMode} from 'state/shell'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from 'lib/routes/types'
import {useSearchPostsQuery} from 'state/queries/search-posts'
import {Post} from 'view/com/post/Post'
import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {HITSLOP_10} from 'lib/constants'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {CommonNavigatorParams} from 'lib/routes/types'
import {shareUrl} from 'lib/sharing'
import {cleanError} from 'lib/strings/errors'
import {sanitizeHandle} from 'lib/strings/handles'
import {enforceLen} from 'lib/strings/helpers'
import {isNative} from 'platform/detection'
import {useSearchPostsQuery} from 'state/queries/search-posts'
import {useSetMinimalShellMode} from 'state/shell'
import {Post} from 'view/com/post/Post'
import {List} from 'view/com/util/List'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox'
import {
ListFooter,
ListHeaderDesktop,
ListMaybePlaceholder,
} from '#/components/Lists'
import {List} from 'view/com/util/List'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {sanitizeHandle} from 'lib/strings/handles'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox'
import {shareUrl} from 'lib/sharing'
import {HITSLOP_10} from 'lib/constants'
import {isNative} from 'platform/detection'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
const renderItem = ({item}: ListRenderItemInfo<PostView>) => {
return <Post post={item} />
@@ -61,9 +63,8 @@ export default function HashtagScreen({
const {
data,
isFetching,
isFetchingNextPage,
isLoading,
isRefetching,
isError,
error,
refetch,
@@ -97,9 +98,9 @@ export default function HashtagScreen({
}, [refetch])
const onEndReached = React.useCallback(() => {
if (isFetching || !hasNextPage || error) return
if (isFetchingNextPage || !hasNextPage || error) return
fetchNextPage()
}, [isFetching, hasNextPage, error, fetchNextPage])
}, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
return (
<>
@@ -123,16 +124,16 @@ export default function HashtagScreen({
: undefined
}
/>
<ListMaybePlaceholder
isLoading={isLoading || isRefetching}
isError={isError}
isEmpty={posts.length < 1}
onRetry={refetch}
emptyTitle="results"
emptyMessage={_(msg`We couldn't find any results for that hashtag.`)}
/>
{!isLoading && posts.length > 0 && (
<List<PostView>
{posts.length < 1 ? (
<ListMaybePlaceholder
isLoading={isLoading}
isError={isError}
onRetry={refetch}
emptyType="results"
emptyMessage={_(msg`We couldn't find any results for that hashtag.`)}
/>
) : (
<List
data={posts}
renderItem={renderItem}
keyExtractor={keyExtractor}
@@ -150,9 +151,8 @@ export default function HashtagScreen({
}
ListFooterComponent={
<ListFooter
isFetching={isFetching && !isRefetching}
isError={isError}
error={error?.name}
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
/>
}
+7 -111
View File
@@ -5,76 +5,15 @@ import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics'
import {logEvent} from '#/lib/statsig/statsig'
import {colors} from '#/lib/styles'
import {useProfileQuery} from '#/state/queries/profile'
import {SessionAccount, useSession, useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import * as Toast from '#/view/com/util/Toast'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {AccountList} from '#/components/AccountList'
import {Button} from '#/components/Button'
import * as TextField from '#/components/forms/TextField'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
import {Text} from '#/components/Typography'
import {FormContainer} from './FormContainer'
function AccountItem({
account,
onSelect,
isCurrentAccount,
}: {
account: SessionAccount
onSelect: (account: SessionAccount) => void
isCurrentAccount: boolean
}) {
const t = useTheme()
const {_} = useLingui()
const {data: profile} = useProfileQuery({did: account.did})
const onPress = React.useCallback(() => {
onSelect(account)
}, [account, onSelect])
return (
<Button
testID={`chooseAccountBtn-${account.handle}`}
key={account.did}
style={[a.flex_1]}
onPress={onPress}
label={
isCurrentAccount
? _(msg`Continue as ${account.handle} (currently signed in)`)
: _(msg`Sign in as ${account.handle}`)
}>
{({hovered, pressed}) => (
<View
style={[
a.flex_1,
a.flex_row,
a.align_center,
{height: 48},
(hovered || pressed) && t.atoms.bg_contrast_25,
]}>
<View style={a.p_md}>
<UserAvatar avatar={profile?.avatar} size={24} />
</View>
<Text style={[a.align_baseline, a.flex_1, a.flex_row, a.py_sm]}>
<Text style={[a.font_bold]}>
{profile?.displayName || account.handle}{' '}
</Text>
<Text style={[t.atoms.text_contrast_medium]}>{account.handle}</Text>
</Text>
{isCurrentAccount ? (
<Check size="sm" style={[{color: colors.green3}, a.mr_md]} />
) : (
<Chevron size="sm" style={[t.atoms.text, a.mr_md]} />
)}
</View>
)}
</Button>
)
}
export const ChooseAccountForm = ({
onSelectAccount,
onPressBack,
@@ -84,8 +23,7 @@ export const ChooseAccountForm = ({
}) => {
const {track, screen} = useAnalytics()
const {_} = useLingui()
const t = useTheme()
const {accounts, currentAccount} = useSession()
const {currentAccount} = useSession()
const {initSession} = useSessionApi()
const {setShowLoggedOut} = useLoggedOutViewControls()
@@ -125,52 +63,10 @@ export const ChooseAccountForm = ({
<TextField.Label>
<Trans>Sign in as...</Trans>
</TextField.Label>
<View
style={[
a.rounded_md,
a.overflow_hidden,
a.border,
t.atoms.border_contrast_low,
]}>
{accounts.map(account => (
<React.Fragment key={account.did}>
<AccountItem
account={account}
onSelect={onSelect}
isCurrentAccount={account.did === currentAccount?.did}
/>
<View style={[a.border_b, t.atoms.border_contrast_low]} />
</React.Fragment>
))}
<Button
testID="chooseNewAccountBtn"
style={[a.flex_1]}
onPress={() => onSelectAccount(undefined)}
label={_(msg`Login to account that is not listed`)}>
{({hovered, pressed}) => (
<View
style={[
a.flex_1,
a.flex_row,
a.align_center,
{height: 48},
(hovered || pressed) && t.atoms.bg_contrast_25,
]}>
<Text
style={[
a.align_baseline,
a.flex_1,
a.flex_row,
a.py_sm,
{paddingLeft: 48},
]}>
<Trans>Other account</Trans>
</Text>
<Chevron size="sm" style={[t.atoms.text, a.mr_md]} />
</View>
)}
</Button>
</View>
<AccountList
onSelectAccount={onSelect}
onSelectOther={() => onSelectAccount()}
/>
</View>
<View style={[a.flex_row]}>
<Button
+10 -31
View File
@@ -1,13 +1,6 @@
import React from 'react'
import {
ScrollView,
type StyleProp,
StyleSheet,
View,
type ViewStyle,
} from 'react-native'
import {type StyleProp, View, type ViewStyle} from 'react-native'
import {isWeb} from '#/platform/detection'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
@@ -16,38 +9,24 @@ export function FormContainer({
title,
children,
style,
contentContainerStyle,
}: {
testID?: string
title?: React.ReactNode
children: React.ReactNode
style?: StyleProp<ViewStyle>
contentContainerStyle?: StyleProp<ViewStyle>
}) {
const {gtMobile} = useBreakpoints()
const t = useTheme()
return (
<ScrollView
<View
testID={testID}
style={[styles.maxHeight, contentContainerStyle]}
keyboardShouldPersistTaps="handled">
<View
style={[a.gap_md, a.flex_1, !gtMobile && [a.px_lg, a.pt_md], style]}>
{title && !gtMobile && (
<Text style={[a.text_xl, a.font_bold, t.atoms.text_contrast_high]}>
{title}
</Text>
)}
{children}
</View>
</ScrollView>
style={[a.gap_md, a.flex_1, !gtMobile && [a.px_lg, a.py_md], style]}>
{title && !gtMobile && (
<Text style={[a.text_xl, a.font_bold, t.atoms.text_contrast_high]}>
{title}
</Text>
)}
{children}
</View>
)
}
const styles = StyleSheet.create({
maxHeight: {
// @ts-ignore web only -prf
maxHeight: isWeb ? '100vh' : undefined,
height: !isWeb ? '100%' : undefined,
},
})
+5 -1
View File
@@ -164,7 +164,11 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
return (
<KeyboardAvoidingView testID="signIn" behavior="padding" style={a.flex_1}>
<LoggedOutLayout leadin="" title={title} description={description}>
<LoggedOutLayout
leadin=""
title={title}
description={description}
scrollable>
<LayoutAnimationConfig skipEntering skipExiting>
<ScreenTransition key={currentForm}>{content}</ScreenTransition>
</LayoutAnimationConfig>
+31 -33
View File
@@ -1,51 +1,49 @@
import React from 'react'
import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {ComAtprotoLabelDefs} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {LABELS} from '@atproto/api'
import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {ComAtprotoLabelDefs} from '@atproto/api'
import {LABELS} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types'
import {CenteredView} from '#/view/com/util/Views'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {useAnalytics} from 'lib/analytics/analytics'
import {useSetMinimalShellMode} from '#/state/shell'
import {useSession} from '#/state/session'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {logger} from '#/logger'
import {
useMyLabelersQuery,
usePreferencesQuery,
UsePreferencesQueryResponse,
usePreferencesSetAdultContentMutation,
} from '#/state/queries/preferences'
import {
useProfileQuery,
useProfileUpdateMutation,
} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useAnalytics} from 'lib/analytics/analytics'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import {ScrollView} from '#/view/com/util/Views'
import {
UsePreferencesQueryResponse,
useMyLabelersQuery,
usePreferencesQuery,
usePreferencesSetAdultContentMutation,
} from '#/state/queries/preferences'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {logger} from '#/logger'
import {useTheme, atoms as a, useBreakpoints, ViewStyleProp} from '#/alf'
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {Divider} from '#/components/Divider'
import * as Toggle from '#/components/forms/Toggle'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
import {Props as SVGIconProps} from '#/components/icons/common'
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
import {Text} from '#/components/Typography'
import * as Toggle from '#/components/forms/Toggle'
import {InlineLink, Link} from '#/components/Link'
import {Button, ButtonText} from '#/components/Button'
import {Loader} from '#/components/Loader'
import * as LabelingService from '#/components/LabelingServiceCard'
import {InlineLink, Link} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {GlobalLabelPreference} from '#/components/moderation/LabelPreference'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {Props as SVGIconProps} from '#/components/icons/common'
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
import * as Dialog from '#/components/Dialog'
import {Text} from '#/components/Typography'
function ErrorState({error}: {error: string}) {
const t = useTheme()
@@ -1,18 +1,18 @@
import React from 'react'
import {View} from 'react-native'
import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {UseMutateFunction} from '@tanstack/react-query'
import {logger} from '#/logger'
import {isIOS} from '#/platform/detection'
import {usePreferencesQuery} from '#/state/queries/preferences'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {logger} from '#/logger'
import {Text} from '#/components/Typography'
import * as Toggle from '#/components/forms/Toggle'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import * as Prompt from '#/components/Prompt'
import {isIOS} from '#/platform/detection'
import {Text} from '#/components/Typography'
function Card({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
@@ -123,9 +123,7 @@ export function AdultContentEnabledPref({
</Trans>
</Prompt.Description>
<Prompt.Actions>
<Prompt.Action onPress={() => prompt.close()}>
<Trans>OK</Trans>
</Prompt.Action>
<Prompt.Action onPress={() => prompt.close()} cta={_(msg`OK`)} />
</Prompt.Actions>
</Prompt.Outer>
</>
@@ -3,43 +3,42 @@ import {View} from 'react-native'
import {
AppBskyActorDefs,
AppBskyLabelerDefs,
ModerationOpts,
moderateProfile,
ModerationOpts,
RichText as RichTextAPI,
} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {RichText} from '#/components/RichText'
import {useModalControls} from '#/state/modals'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAnalytics} from 'lib/analytics/analytics'
import {useSession} from '#/state/session'
import {Haptics} from '#/lib/haptics'
import {isAppLabeler} from '#/lib/moderation'
import {pluralize} from '#/lib/strings/helpers'
import {logger} from '#/logger'
import {Shadow} from '#/state/cache/types'
import {useProfileShadow} from 'state/cache/profile-shadow'
import {useModalControls} from '#/state/modals'
import {useLabelerSubscriptionMutation} from '#/state/queries/labeler'
import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
import {logger} from '#/logger'
import {Haptics} from '#/lib/haptics'
import {pluralize} from '#/lib/strings/helpers'
import {isAppLabeler} from '#/lib/moderation'
import {atoms as a, useTheme, tokens} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {Text} from '#/components/Typography'
import * as Toast from '#/view/com/util/Toast'
import {ProfileHeaderShell} from './Shell'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {useAnalytics} from 'lib/analytics/analytics'
import {useProfileShadow} from 'state/cache/profile-shadow'
import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, tokens, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {DialogOuterProps} from '#/components/Dialog'
import {
Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
Heart2_Stroke2_Corner0_Rounded as Heart,
} from '#/components/icons/Heart2'
import {Link} from '#/components/Link'
import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import {ProfileHeaderDisplayName} from './DisplayName'
import {ProfileHeaderHandle} from './Handle'
import {ProfileHeaderMetrics} from './Metrics'
import {
Heart2_Stroke2_Corner0_Rounded as Heart,
Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
} from '#/components/icons/Heart2'
import {DialogOuterProps} from '#/components/Dialog'
import * as Prompt from '#/components/Prompt'
import {Link} from '#/components/Link'
import {ProfileHeaderShell} from './Shell'
interface Props {
profile: AppBskyActorDefs.ProfileViewDetailed
@@ -243,6 +242,8 @@ let ProfileHeaderLabeler = ({
style={[a.text_md]}
numberOfLines={15}
value={descriptionRT}
enableTags
authorHandle={profile.handle}
/>
</View>
) : undefined}
@@ -312,6 +313,7 @@ function CantSubscribePrompt({
}: {
control: DialogOuterProps['control']
}) {
const {_} = useLingui()
return (
<Prompt.Outer control={control}>
<Prompt.Title>Unable to subscribe</Prompt.Title>
@@ -322,7 +324,7 @@ function CantSubscribePrompt({
</Trans>
</Prompt.Description>
<Prompt.Actions>
<Prompt.Action onPress={control.close}>OK</Prompt.Action>
<Prompt.Action onPress={control.close} cta={_(msg`OK`)} />
</Prompt.Actions>
</Prompt.Outer>
)
@@ -2,39 +2,38 @@ import React, {memo, useMemo} from 'react'
import {View} from 'react-native'
import {
AppBskyActorDefs,
ModerationOpts,
moderateProfile,
ModerationOpts,
RichText as RichTextAPI,
} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useAnalytics} from 'lib/analytics/analytics'
import {useSession, useRequireAuth} from '#/state/session'
import {Shadow} from '#/state/cache/types'
import {useProfileShadow} from 'state/cache/profile-shadow'
import {
useProfileFollowMutationQueue,
useProfileBlockMutationQueue,
} from '#/state/queries/profile'
import {logger} from '#/logger'
import {Shadow} from '#/state/cache/types'
import {useModalControls} from '#/state/modals'
import {
useProfileBlockMutationQueue,
useProfileFollowMutationQueue,
} from '#/state/queries/profile'
import {useRequireAuth, useSession} from '#/state/session'
import {useAnalytics} from 'lib/analytics/analytics'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText, ButtonIcon} from '#/components/Button'
import * as Toast from '#/view/com/util/Toast'
import {ProfileHeaderShell} from './Shell'
import {useProfileShadow} from 'state/cache/profile-shadow'
import {ProfileHeaderSuggestedFollows} from '#/view/com/profile/ProfileHeaderSuggestedFollows'
import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import {ProfileHeaderDisplayName} from './DisplayName'
import {ProfileHeaderHandle} from './Handle'
import {ProfileHeaderMetrics} from './Metrics'
import {ProfileHeaderSuggestedFollows} from '#/view/com/profile/ProfileHeaderSuggestedFollows'
import {RichText} from '#/components/RichText'
import * as Prompt from '#/components/Prompt'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {ProfileHeaderShell} from './Shell'
interface Props {
profile: AppBskyActorDefs.ProfileViewDetailed
@@ -248,6 +247,8 @@ let ProfileHeaderStandard = ({
style={[a.text_md]}
numberOfLines={15}
value={descriptionRT}
enableTags
authorHandle={profile.handle}
/>
</View>
) : undefined}
+4 -17
View File
@@ -4,13 +4,11 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {useSetMinimalShellMode} from '#/state/shell'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {LikedByList} from '#/components/LikedByList'
import {useSetMinimalShellMode} from '#/state/shell'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {atoms as a, useBreakpoints} from '#/alf'
export function ProfileLabelerLikedByScreen({
route,
@@ -19,7 +17,6 @@ export function ProfileLabelerLikedByScreen({
const {name: handleOrDid} = route.params
const uri = makeRecordUri(handleOrDid, 'app.bsky.labeler.service', 'self')
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
useFocusEffect(
React.useCallback(() => {
@@ -28,17 +25,7 @@ export function ProfileLabelerLikedByScreen({
)
return (
<View
style={[
a.mx_auto,
a.w_full,
a.h_full_vh,
gtMobile && [
{
maxWidth: 600,
},
],
]}>
<View style={{flex: 1}}>
<ViewHeader title={_(msg`Liked By`)} />
<LikedByList uri={uri} />
</View>
+13 -14
View File
@@ -1,30 +1,29 @@
import React from 'react'
import {View} from 'react-native'
import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {
AppBskyLabelerDefs,
ModerationOpts,
interpretLabelValueDefinitions,
InterpretedLabelValueDefinition,
interpretLabelValueDefinitions,
ModerationOpts,
} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {useScrollHandlers} from '#/lib/ScrollContext'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
import {ListRef} from '#/view/com/util/List'
import {SectionRef} from './types'
import {useScrollHandlers} from '#/lib/ScrollContext'
import {isNative} from '#/platform/detection'
import {useTheme, atoms as a} from '#/alf'
import {Text} from '#/components/Typography'
import {Loader} from '#/components/Loader'
import {Divider} from '#/components/Divider'
import {ListRef} from '#/view/com/util/List'
import {CenteredView, ScrollView} from '#/view/com/util/Views'
import {ErrorState} from '../ErrorState'
import {LabelerLabelPreference} from '#/components/moderation/LabelPreference'
import {atoms as a, useTheme} from '#/alf'
import {Divider} from '#/components/Divider'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {Loader} from '#/components/Loader'
import {LabelerLabelPreference} from '#/components/moderation/LabelPreference'
import {Text} from '#/components/Typography'
import {ErrorState} from '../ErrorState'
import {SectionRef} from './types'
interface LabelsSectionProps {
isLabelerLoading: boolean
+9 -24
View File
@@ -1,11 +1,10 @@
import React from 'react'
import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {ActivityIndicator, View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {nanoid} from 'nanoid/non-secure'
import {createFullHandle} from '#/lib/strings/handles'
import {isWeb} from '#/platform/detection'
import {ScreenTransition} from '#/screens/Login/ScreenTransition'
import {useSignupContext, useSubmitSignup} from '#/screens/Signup/state'
import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView'
@@ -54,7 +53,14 @@ export function StepCaptcha() {
return (
<ScreenTransition>
<View style={[a.gap_lg]}>
<View style={[styles.container, completed && styles.center]}>
<View
style={[
a.w_full,
a.pb_xl,
a.overflow_hidden,
{minHeight: 500},
completed && [a.align_center, a.justify_center],
]}>
{!completed ? (
<CaptchaWebView
url={url}
@@ -72,24 +78,3 @@ export function StepCaptcha() {
</ScreenTransition>
)
}
const styles = StyleSheet.create({
error: {
borderRadius: 6,
marginTop: 10,
},
// @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832.
touchable: {
...(isWeb && {cursor: 'pointer'}),
},
container: {
minHeight: 500,
width: '100%',
paddingBottom: 20,
overflow: 'hidden',
},
center: {
alignItems: 'center',
justifyContent: 'center',
},
})
+1 -1
View File
@@ -76,7 +76,7 @@ export const Policies = ({
{under13 ? (
<Text style={[a.font_bold, a.leading_snug, t.atoms.text_contrast_high]}>
You must be 13 years of age or older to sign up.
<Trans>You must be 13 years of age or older to sign up.</Trans>
</Text>
) : needsGuardian ? (
<Text style={[a.font_bold, a.leading_snug, t.atoms.text_contrast_high]}>
+36 -23
View File
@@ -1,5 +1,6 @@
import React from 'react'
import {ScrollView, View} from 'react-native'
import {View} from 'react-native'
import {LayoutAnimationConfig} from 'react-native-reanimated'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -20,7 +21,7 @@ import {
import {StepCaptcha} from '#/screens/Signup/StepCaptcha'
import {StepHandle} from '#/screens/Signup/StepHandle'
import {StepInfo} from '#/screens/Signup/StepInfo'
import {atoms as a, useTheme} from '#/alf'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {InlineLink} from '#/components/Link'
@@ -32,6 +33,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
const {screen} = useAnalytics()
const [state, dispatch] = React.useReducer(reducer, initialState)
const submit = useSubmitSignup({state, dispatch})
const {gtMobile} = useBreakpoints()
const {
data: serviceInfo,
@@ -125,13 +127,16 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
<LoggedOutLayout
leadin=""
title={_(msg`Create Account`)}
description={_(msg`We're so excited to have you join us!`)}>
<ScrollView
testID="createAccount"
keyboardShouldPersistTaps="handled"
style={a.h_full}
keyboardDismissMode="on-drag">
<View style={[a.flex_1, a.px_xl, a.pt_2xl, {paddingBottom: 100}]}>
description={_(msg`We're so excited to have you join us!`)}
scrollable>
<View testID="createAccount" style={a.flex_1}>
<View
style={[
a.flex_1,
a.px_xl,
a.pt_2xl,
!gtMobile && {paddingBottom: 100},
]}>
<View style={[a.gap_sm, a.pb_3xl]}>
<Text style={[a.font_semibold, t.atoms.text_contrast_medium]}>
<Trans>Step</Trans> {state.activeStep + 1} <Trans>of</Trans>{' '}
@@ -152,45 +157,53 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
</View>
<View style={[a.pb_3xl]}>
{state.activeStep === SignupStep.INFO ? (
<StepInfo />
) : state.activeStep === SignupStep.HANDLE ? (
<StepHandle />
) : (
<StepCaptcha />
)}
<LayoutAnimationConfig skipEntering skipExiting>
{state.activeStep === SignupStep.INFO ? (
<StepInfo />
) : state.activeStep === SignupStep.HANDLE ? (
<StepHandle />
) : (
<StepCaptcha />
)}
</LayoutAnimationConfig>
</View>
<View style={[a.flex_row, a.justify_between, a.pb_lg]}>
<Button
label="Back"
label={_(msg`Go back to previous step`)}
variant="solid"
color="secondary"
size="medium"
onPress={onBackPress}>
Back
<ButtonText>
<Trans>Back</Trans>
</ButtonText>
</Button>
{state.activeStep !== SignupStep.CAPTCHA && (
<>
{isError ? (
<Button
label="Retry"
label={_(msg`Press to retry`)}
variant="solid"
color="primary"
size="medium"
disabled={state.isLoading}
onPress={() => refetch()}>
Retry
<ButtonText>
<Trans>Retry</Trans>
</ButtonText>
</Button>
) : (
<Button
label="Next"
label={_(msg`Continue to next step`)}
variant="solid"
color="primary"
size="medium"
disabled={!state.canNext || state.isLoading}
onPress={onNextPress}>
<ButtonText>Next</ButtonText>
<ButtonText>
<Trans>Next</Trans>
</ButtonText>
</Button>
)}
</>
@@ -208,7 +221,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
</Text>
</View>
</View>
</ScrollView>
</View>
</LoggedOutLayout>
</SignupContext.Provider>
)
+4 -8
View File
@@ -1,11 +1,11 @@
import React from 'react'
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
import {ImageModel} from '#/state/models/media/image'
import {GalleryModel} from '#/state/models/media/gallery'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {EmbedPlayerSource} from '#/lib/strings/embed-player'
import {GalleryModel} from '#/state/models/media/gallery'
import {ImageModel} from '#/state/models/media/image'
import {ThreadgateSetting} from '../queries/threadgate'
export interface EditProfileModal {
@@ -118,14 +118,11 @@ export interface ChangePasswordModal {
name: 'change-password'
}
export interface SwitchAccountModal {
name: 'switch-account'
}
export interface LinkWarningModal {
name: 'link-warning'
text: string
href: string
share?: boolean
}
export interface EmbedConsentModal {
@@ -148,7 +145,6 @@ export type Modal =
| VerifyEmailModal
| ChangeEmailModal
| ChangePasswordModal
| SwitchAccountModal
// Curation
| ContentLanguagesSettingsModal
+11 -10
View File
@@ -1,21 +1,22 @@
import React from 'react'
import {AppBskyActorDefs, ModerationOpts, moderateProfile} from '@atproto/api'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {useQuery, useQueryClient} from '@tanstack/react-query'
import {logger} from '#/logger'
import {getAgent} from '#/state/session'
import {useMyFollowsQuery} from '#/state/queries/my-follows'
import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES, useModerationOpts} from './preferences'
import {isInvalidHandle} from '#/lib/strings/handles'
import {isJustAMute} from '#/lib/moderation'
import {isInvalidHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {useMyFollowsQuery} from '#/state/queries/my-follows'
import {getAgent} from '#/state/session'
import {DEFAULT_LOGGED_OUT_PREFERENCES, useModerationOpts} from './preferences'
const DEFAULT_MOD_OPTS = {
userDid: undefined,
prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs,
}
export const RQKEY = (prefix: string) => ['actor-autocomplete', prefix]
const RQKEY_ROOT = 'actor-autocomplete'
export const RQKEY = (prefix: string) => [RQKEY_ROOT, prefix]
export function useActorAutocompleteQuery(prefix: string) {
const {data: follows, isFetching} = useMyFollowsQuery()
@@ -29,7 +30,7 @@ export function useActorAutocompleteQuery(prefix: string) {
async queryFn() {
const res = prefix
? await getAgent().searchActorsTypeahead({
term: prefix,
q: prefix,
limit: 8,
})
: undefined
@@ -67,7 +68,7 @@ export function useActorAutocompleteFn() {
queryKey: RQKEY(query || ''),
queryFn: () =>
getAgent().searchActorsTypeahead({
term: query,
q: query,
limit,
}),
})
+5 -4
View File
@@ -1,10 +1,11 @@
import {AppBskyActorDefs} from '@atproto/api'
import {QueryClient, useQuery} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
export const RQKEY = (prefix: string) => ['actor-search', prefix]
const RQKEY_ROOT = 'actor-search'
export const RQKEY = (prefix: string) => [RQKEY_ROOT, prefix]
export function useActorSearch(prefix: string) {
return useQuery<AppBskyActorDefs.ProfileView[]>({
@@ -12,7 +13,7 @@ export function useActorSearch(prefix: string) {
queryKey: RQKEY(prefix || ''),
async queryFn() {
const res = await getAgent().searchActors({
term: prefix,
q: prefix,
})
return res.data.actors
},
@@ -26,7 +27,7 @@ export function* findAllProfilesInQueryData(
) {
const queryDatas = queryClient.getQueriesData<AppBskyActorDefs.ProfileView[]>(
{
queryKey: ['actor-search'],
queryKey: [RQKEY_ROOT],
},
)
for (const [_queryKey, queryData] of queryDatas) {
+3 -2
View File
@@ -1,10 +1,11 @@
import {ComAtprotoServerCreateAppPassword} from '@atproto/api'
import {useQuery, useQueryClient, useMutation} from '@tanstack/react-query'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {getAgent} from '../session'
export const RQKEY = () => ['app-passwords']
const RQKEY_ROOT = 'app-passwords'
export const RQKEY = () => [RQKEY_ROOT]
export function useAppPasswordsQuery() {
return useQuery({
+17 -14
View File
@@ -1,24 +1,24 @@
import {
useQuery,
useInfiniteQuery,
InfiniteData,
QueryKey,
useMutation,
} from '@tanstack/react-query'
import {
AtUri,
RichText,
AppBskyFeedDefs,
AppBskyGraphDefs,
AppBskyUnspeccedGetPopularFeedGenerators,
AtUri,
RichText,
} from '@atproto/api'
import {
InfiniteData,
QueryKey,
useInfiniteQuery,
useMutation,
useQuery,
} from '@tanstack/react-query'
import {router} from '#/routes'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {getAgent} from '#/state/session'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {getAgent} from '#/state/session'
import {router} from '#/routes'
export type FeedSourceFeedInfo = {
type: 'feed'
@@ -56,8 +56,9 @@ export type FeedSourceListInfo = {
export type FeedSourceInfo = FeedSourceFeedInfo | FeedSourceListInfo
const feedSourceInfoQueryKeyRoot = 'getFeedSourceInfo'
export const feedSourceInfoQueryKey = ({uri}: {uri: string}) => [
'getFeedSourceInfo',
feedSourceInfoQueryKeyRoot,
uri,
]
@@ -216,6 +217,8 @@ const FOLLOWING_FEED_STUB: FeedSourceInfo = {
likeUri: '',
}
const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos'
export function usePinnedFeedsInfos() {
const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
const pinnedUris = preferences?.feeds?.pinned ?? []
@@ -223,7 +226,7 @@ export function usePinnedFeedsInfos() {
return useQuery({
staleTime: STALE.INFINITY,
enabled: !isLoadingPrefs,
queryKey: ['pinnedFeedsInfos', pinnedUris.join(',')],
queryKey: [pinnedFeedInfosQueryKeyRoot, pinnedUris.join(',')],
queryFn: async () => {
let resolved = new Map()
+9 -4
View File
@@ -1,11 +1,16 @@
import React from 'react'
import {useQueryClient, useMutation} from '@tanstack/react-query'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
const fetchHandleQueryKey = (handleOrDid: string) => ['handle', handleOrDid]
const fetchDidQueryKey = (handleOrDid: string) => ['did', handleOrDid]
const handleQueryKeyRoot = 'handle'
const fetchHandleQueryKey = (handleOrDid: string) => [
handleQueryKeyRoot,
handleOrDid,
]
const didQueryKeyRoot = 'did'
const fetchDidQueryKey = (handleOrDid: string) => [didQueryKeyRoot, handleOrDid]
export function useFetchHandle() {
const queryClient = useQueryClient()
+5 -3
View File
@@ -1,14 +1,16 @@
import {ComAtprotoServerDefs} from '@atproto/api'
import {useQuery} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
import {cleanError} from '#/lib/strings/errors'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
function isInviteAvailable(invite: ComAtprotoServerDefs.InviteCode): boolean {
return invite.available - invite.uses.length > 0 && !invite.disabled
}
const inviteCodesQueryKeyRoot = 'inviteCodes'
export type InviteCodesQueryResponse = Exclude<
ReturnType<typeof useInviteCodesQuery>['data'],
undefined
@@ -16,7 +18,7 @@ export type InviteCodesQueryResponse = Exclude<
export function useInviteCodesQuery() {
return useQuery({
staleTime: STALE.MINUTES.FIVE,
queryKey: ['inviteCodes'],
queryKey: [inviteCodesQueryKeyRoot],
queryFn: async () => {
const res = await getAgent()
.com.atproto.server.getAccountInviteCodes({})
+17 -9
View File
@@ -1,18 +1,26 @@
import {z} from 'zod'
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
import {AppBskyLabelerDefs} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {z} from 'zod'
import {getAgent} from '#/state/session'
import {preferencesQueryKey} from '#/state/queries/preferences'
import {labelersDetailedInfoQueryKeyRoot} from '#/lib/react-query'
import {STALE} from '#/state/queries'
import {preferencesQueryKey} from '#/state/queries/preferences'
import {getAgent} from '#/state/session'
export const labelerInfoQueryKey = (did: string) => ['labeler-info', did]
export const labelersInfoQueryKey = (dids: string[]) => [
'labelers-info',
dids.sort(),
const labelerInfoQueryKeyRoot = 'labeler-info'
export const labelerInfoQueryKey = (did: string) => [
labelerInfoQueryKeyRoot,
did,
]
const labelersInfoQueryKeyRoot = 'labelers-info'
export const labelersInfoQueryKey = (dids: string[]) => [
labelersInfoQueryKeyRoot,
dids.slice().sort(),
]
export const labelersDetailedInfoQueryKey = (dids: string[]) => [
'labelers-detailed-info',
labelersDetailedInfoQueryKeyRoot,
dids,
]
+5 -4
View File
@@ -1,18 +1,19 @@
import {AppBskyActorDefs, AppBskyGraphGetList} from '@atproto/api'
import {
useInfiniteQuery,
InfiniteData,
QueryClient,
QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
import {getAgent} from '#/state/session'
const PAGE_SIZE = 30
type RQPageParam = string | undefined
export const RQKEY = (uri: string) => ['list-members', uri]
const RQKEY_ROOT = 'list-members'
export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
export function useListMembersQuery(uri: string) {
return useInfiniteQuery<
@@ -44,7 +45,7 @@ export function* findAllProfilesInQueryData(
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetList.OutputSchema>
>({
queryKey: ['list-members'],
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData) {
+4 -3
View File
@@ -17,16 +17,17 @@
import {AtUri} from '@atproto/api'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {useSession, getAgent} from '#/state/session'
import {RQKEY as LIST_MEMBERS_RQKEY} from '#/state/queries/list-members'
import {STALE} from '#/state/queries'
import {RQKEY as LIST_MEMBERS_RQKEY} from '#/state/queries/list-members'
import {getAgent, useSession} from '#/state/session'
// sanity limit is SANITY_PAGE_LIMIT*PAGE_SIZE total records
const SANITY_PAGE_LIMIT = 1000
const PAGE_SIZE = 100
// ...which comes 100,000k list members
export const RQKEY = () => ['list-memberships']
const RQKEY_ROOT = 'list-memberships'
export const RQKEY = () => [RQKEY_ROOT]
export interface ListMembersip {
membershipUri: string
+10 -8
View File
@@ -1,21 +1,23 @@
import {Image as RNImage} from 'react-native-image-crop-picker'
import {
AtUri,
AppBskyGraphDefs,
AppBskyGraphGetList,
AppBskyGraphList,
AppBskyGraphDefs,
AtUri,
Facet,
} from '@atproto/api'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import chunk from 'lodash.chunk'
import {useSession, getAgent} from '../session'
import {invalidate as invalidateMyLists} from './my-lists'
import {RQKEY as PROFILE_LISTS_RQKEY} from './profile-lists'
import {uploadBlob} from '#/lib/api'
import {until} from '#/lib/async/until'
import {STALE} from '#/state/queries'
import {getAgent, useSession} from '../session'
import {invalidate as invalidateMyLists} from './my-lists'
import {RQKEY as PROFILE_LISTS_RQKEY} from './profile-lists'
export const RQKEY = (uri: string) => ['list', uri]
const RQKEY_ROOT = 'list'
export const RQKEY = (uri: string) => [RQKEY_ROOT, uri]
export function useListQuery(uri?: string) {
return useQuery<AppBskyGraphDefs.ListView, Error>({
+4 -3
View File
@@ -1,14 +1,15 @@
import {AppBskyActorDefs, AppBskyGraphGetBlocks} from '@atproto/api'
import {
useInfiniteQuery,
InfiniteData,
QueryClient,
QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
export const RQKEY = () => ['my-blocked-accounts']
const RQKEY_ROOT = 'my-blocked-accounts'
export const RQKEY = () => [RQKEY_ROOT]
type RQPageParam = string | undefined
export function useMyBlockedAccountsQuery() {
@@ -39,7 +40,7 @@ export function* findAllProfilesInQueryData(
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetBlocks.OutputSchema>
>({
queryKey: ['my-blocked-accounts'],
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {
+4 -2
View File
@@ -1,14 +1,16 @@
import {AppBskyActorDefs} from '@atproto/api'
import {useQuery} from '@tanstack/react-query'
import {useSession, getAgent} from '../session'
import {STALE} from '#/state/queries'
import {getAgent, useSession} from '../session'
// sanity limit is SANITY_PAGE_LIMIT*PAGE_SIZE total records
const SANITY_PAGE_LIMIT = 1000
const PAGE_SIZE = 100
// ...which comes 10,000k follows
export const RQKEY = () => ['my-follows']
const RQKEY_ROOT = 'my-follows'
export const RQKEY = () => [RQKEY_ROOT]
export function useMyFollowsQuery() {
const {currentAccount} = useSession()
+6 -4
View File
@@ -1,16 +1,18 @@
import {AppBskyGraphDefs} from '@atproto/api'
import {useQuery, QueryClient} from '@tanstack/react-query'
import {QueryClient, useQuery} from '@tanstack/react-query'
import {accumulate} from '#/lib/async/accumulate'
import {useSession, getAgent} from '#/state/session'
import {STALE} from '#/state/queries'
import {getAgent, useSession} from '#/state/session'
export type MyListsFilter =
| 'all'
| 'curate'
| 'mod'
| 'all-including-subscribed'
export const RQKEY = (filter: MyListsFilter) => ['my-lists', filter]
const RQKEY_ROOT = 'my-lists'
export const RQKEY = (filter: MyListsFilter) => [RQKEY_ROOT, filter]
export function useMyListsQuery(filter: MyListsFilter) {
const {currentAccount} = useSession()
@@ -91,6 +93,6 @@ export function invalidate(qc: QueryClient, filter?: MyListsFilter) {
if (filter) {
qc.invalidateQueries({queryKey: RQKEY(filter)})
} else {
qc.invalidateQueries({queryKey: ['my-lists']})
qc.invalidateQueries({queryKey: [RQKEY_ROOT]})
}
}
+4 -3
View File
@@ -1,14 +1,15 @@
import {AppBskyActorDefs, AppBskyGraphGetMutes} from '@atproto/api'
import {
useInfiniteQuery,
InfiniteData,
QueryClient,
QueryKey,
useInfiniteQuery,
} from '@tanstack/react-query'
import {getAgent} from '#/state/session'
export const RQKEY = () => ['my-muted-accounts']
const RQKEY_ROOT = 'my-muted-accounts'
export const RQKEY = () => [RQKEY_ROOT]
type RQPageParam = string | undefined
export function useMyMutedAccountsQuery() {
@@ -39,7 +40,7 @@ export function* findAllProfilesInQueryData(
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyGraphGetMutes.OutputSchema>
>({
queryKey: ['my-muted-accounts'],
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData?.pages) {

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