Compare commits

..

98 Commits

Author SHA1 Message Date
Samuel Newman d72b21b3a3 rm unused follow button 2025-11-05 10:07:30 +02:00
pfrazee 2077699a07 Nightly source-language update 2025-11-05 02:40:28 +00:00
Samuel Newman fb3f1c3936 always send #interactionReply (#9330) 2025-11-04 19:21:55 +02:00
Samuel Newman 78e20c4200 Prevent sparse arrays in snapToOffsets (#9329)
* prevent sparse arrays in snapToOffsets

* wait, sparse arrays don't work like that

* more readable check even if it's technically useless

* make the second check actually work
2025-11-04 09:08:43 -08:00
Eric Bailey d8e7e65a9d Post skele tweaks (#9328)
* feat: PostControlsSkeleton (#9205)

* feat: new PostControlsSkeleton component

* feat: integrate PostControlsSkeleton into other usages

* fix: shrink skele pill and circle sizes a bit

* Couple small tweaks

(cherry picked from commit f6a38ec42274bffc41b2898d8addb735791494fd)

---------

Co-authored-by: Elijah Seed-Arita <elijaharita@gmail.com>
2025-11-04 10:09:17 -06:00
Samuel Newman 16b14b9d13 Restore await to dismissBrowser in intent handler (#9326) 2025-11-04 16:57:52 +02:00
Samuel Newman fd5b1b3a59 add even more optional chaining to link thing (#9327) 2025-11-04 16:57:39 +02:00
Samuel Newman c9033dbc39 Avoid asking for read access to photo library when saving images on iOS (#9297)
* use saveToLibraryAsync on iOS

* use _(msg``) syntax
2025-11-04 06:33:30 -08:00
Samuel Newman 1b0fbd503e Remove use of flatten where possible (#9325)
* remove use of flatten where possible

* restore flatten where it says it's needed

* missing utils alpha
2025-11-04 05:43:08 -08:00
pfrazee 0bcde99025 Nightly source-language update 2025-11-04 02:40:47 +00:00
Alex Benzer 5a4e012fdc Adds client event for empty feed error (#9287)
* Adds client event for empty feed error

* Only log for discover

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2025-11-03 11:37:40 -06:00
Samuel Newman 6ccedd5e31 ship post follow follow suggestions (#9251) 2025-11-03 09:18:49 -08:00
Samuel Newman feab782ba9 Android lightbox minor perf improvement (#9274)
* minor perf improvement on android for lightbox

* Use correct `cachePolicy` when prefetching lightbox images (#9275)

* use `memory` cachePolicy when prefetching

* use `memory` cache policy on iOS lightbox images
2025-11-03 09:18:26 -08:00
Samuel Newman ca505fbbd2 chore: update caniuse (#9239) 2025-11-03 09:18:02 -08:00
Samuel Newman e5f3131644 fix right nav overflow (#9253) 2025-11-03 09:15:29 -08:00
Samuel Newman 54a388d132 push in post-post toast (#9252) 2025-11-03 09:15:12 -08:00
Samuel Newman b1f1b2050c add android ripple effect to images in feed (#9273) 2025-11-03 09:13:22 -08:00
Samuel Newman 9070fa9a8d Improve image download timeout handling (#9276) 2025-11-03 09:11:29 -08:00
Samuel Newman 55e9298ee8 disable wake-from-background OTA on Android (#9317) 2025-11-03 09:08:34 -08:00
Samuel Newman c6215065bd Update Android adaptive icon (#9298)
* remove adaptive icon gradient background

* try new foreground/monochrome assets

* adjust size back down
2025-11-03 09:05:18 -08:00
Samuel Newman bd3594ceea use 0.5px media border on high DPI screens on web (#9311) 2025-10-31 07:07:43 -07:00
Samuel Newman de6d2f1912 Update web dim <html> background color (#9307)
* update dim background color

* Update other css colors

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2025-10-30 11:44:22 -05:00
Jason Ericson faa49feec8 Fixed Apple Music song embeds. (#9220)
* Fixed Apple Music song embeds  when using a direct song link (vs playlist or album).

* Fixed song embed test.

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Jason Ericson <jasonmarkericson@gmail.com>
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-30 09:25:42 -07:00
pfrazee 55b3474ad2 Nightly source-language update 2025-10-30 02:39:59 +00:00
Anastasiya Uraleva b0eb079ff8 [APP-1321]: Update brand embeds more clearly (#8955)
* Nightly source-language update

* run prettier

* wip

* feat: use local dev for bskyembed test.html

* feat: commit generated files which i'm not sure about

* Revert "feat: commit generated files which i'm not sure about"

This reverts commit b891efdaa1fd7ddd4417bfd5267e861a25a93b0e.

* chore: revert change to bskyembed/.eslintrc.cjs

* chore: remove generated file from bskyweb/static

* chore: revert messages.po changes

* Update bskyembed/test.html

* Nightly source-language update

* Update: Bluesky embeds

* update: embed colors for light and dark modes

* remove unused changes

* remove duplicate word

* update border radius and add cursor pointer

* add icons folder for dynamic icons

---------

Co-authored-by: Anastasiya Uraleva <anastasiya@Mac.localdomain>
Co-authored-by: Elijah Seed-Arita <elijaharita@gmail.com>
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: Anastasiya Uraleva <anastasiya@Anastasiyas-MacBook-Pro.local>
2025-10-29 17:24:48 -07:00
Samuel Newman cfa2534ccb remove await on dismissBrowser (#9304) 2025-10-29 21:23:10 +02:00
Samuel Newman 2d626f7bbf Fix android icon file name (#9303) 2025-10-29 09:58:56 -07:00
Samuel Newman 5efae600dd Add dark/tinted icon variants (#9241)
* add tinted icon style

* new dark icon

* use icon composer file

* rename some icons, ensure default ios is still there

* use solid fill instead of auto gradient

* fix web build

* back to gradient

* add . for consistency

* rename `default_old` to `legacy`
2025-10-29 09:49:45 -07:00
Eric Bailey 339f3320c6 [TS-147] Add internal debug field display (#9302)
* Add internal debug field display

* Update title
2025-10-29 11:21:19 -05:00
Samuel Newman 3107efb449 Get rid of share menu fade edge effect (#9250) 2025-10-29 09:09:07 -07:00
Maxym Hyba faa389e060 rm clamp (#9271) 2025-10-29 08:46:21 -07:00
Samuel Newman f15a949ef8 more text elements missing emoji prop (#9301) 2025-10-29 08:44:52 -07:00
Samuel Newman bb908a5534 fix line height in hover card (#9281) 2025-10-29 08:44:17 -07:00
Samuel Newman a503fe27f5 add emoji prop to image alt text (#9278) 2025-10-29 08:43:53 -07:00
Chenyu d58c6c9ebc use admonition instead of toast for errors when appealing labels (#9272)
* use admonition instead of toast for errors when appealing labels

* address commments
2025-10-29 06:48:23 -07:00
Caidan bd5d102826 fix: prevent profile suggestions animation when no data available (#9277)
Prevents AccordionAnimation from animating open on web when suggestions array is empty by adding conditional rendering logic to AnimatedProfileHeaderSuggestedFollows component.
2025-10-29 02:43:18 -07:00
pfrazee 32bf6cf01f Nightly source-language update 2025-10-29 02:41:25 +00:00
Samuel Newman e3f5967407 Android - add fallbacks when saving image to gallery (#9293)
* add fallback paths when saving image to gallery

* `debug` -> `info`
2025-10-28 09:58:41 -07:00
Samuel Newman 900e569ef2 Override Reduce Motion on load-bearing animation (#9294) 2025-10-28 09:52:31 -07:00
Samuel Newman 1883f3fc7b Close IAB when it triggers an intent (#8454)
* Close IAB if it opens a link to the app

* native only

* await close
2025-10-28 08:52:09 -07:00
Samuel Newman 5cff45bdc8 Update expo and related dependencies (#9291)
* update expo + deps

* reanimated patch right version

* Update yarn.lock

* update other rn packages
2025-10-28 06:44:16 -07:00
Samuel Newman 0c6469f272 Change adaptive icon background color (#9286) 2025-10-28 00:26:52 -07:00
pfrazee b98350f927 Nightly source-language update 2025-10-28 02:38:50 +00:00
Samuel Newman 206f52fb13 Bump version to 1.110.0 (#9284) 2025-10-27 21:55:55 +02:00
Samuel Newman 78c8a6037f Remove feed:seen from statsig (#9261) 2025-10-27 13:23:38 -05:00
bnewbold 28e132ae03 clarify in content hider if label is on overall account (#9264)
* clarify in content hider if label is on overall account

* fix bool typo

* Update src/components/moderation/ContentHider.tsx

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

* rename variable for clarity

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2025-10-27 10:37:47 -07:00
Samuel Newman 25a2a2c7f6 Revert "Disable Sentry sampling for now (#9262)" (#9282)
This reverts commit de68915bc0.
2025-10-27 11:12:27 -05:00
Eric Bailey 2a4b825637 Release 1.109.0 i18n take III (#9269) 2025-10-23 11:27:49 -05:00
Eric Bailey c00b3d1050 Don't log errors for known app password issues in chat (#9268) 2025-10-23 11:18:53 -05:00
Eric Bailey 74cb81b65f Add config to disable update overlay, disable it (#9267) 2025-10-23 11:16:55 -05:00
Samuel Newman e28aa7d1ab Patch out PerformanceMonitor component (#9266)
* patch out perf monitor component

* make PerformanceMonitor a noop

* rm debug stuff
2025-10-23 10:35:37 -05:00
Eric Bailey d4d5d6d8b9 intl:pull and intl:extract:all (#9263) 2025-10-22 15:51:19 -05:00
Eric Bailey de68915bc0 Disable Sentry sampling for now (#9262) 2025-10-22 14:08:25 -05:00
Samuel Newman ea00663e7f Android CI - Clear cache on second build (#9223)
* clear cache on second build

* alternative cache cleaning method
2025-10-22 10:05:00 -07:00
Samuel Newman 12e57378e8 fix android testflight ci (#9237) 2025-10-22 10:04:36 -07:00
Eric Bailey 24606dbcfc Don't need to be logging upstream errors (#9260) 2025-10-22 11:39:14 -05:00
Eric Bailey 6f31121e51 Check for network error before reporting push token issue (#9258) 2025-10-22 11:36:40 -05:00
Eric Bailey 1abc96314b Don't log network errors (#9257) 2025-10-22 11:36:32 -05:00
Eric Bailey 7cc897d096 Remove error log for Bluesky feed service issues (#9256) 2025-10-22 11:36:24 -05:00
Eric Bailey 6ed30213c8 Add 'Upstream service unreachable' to network errors (#9255) 2025-10-22 11:36:18 -05:00
Eric Bailey d7e8429753 Get iOS debug files into Sentry (#9242)
* Add buildArtifactPaths to iOS builds

* Update privacyManifests

* Revert changes to build configs

* Add --include-sources flag

* Add nativeCrash tester

* Sam's crash testbed

(cherry picked from commit b0a0cb7799cd900777570cae7dc10461cf811e0f)

* Output artifacts, tarball, extract and upload dSYM to Sentry

* Revert "Sam's crash testbed"

This reverts commit cc2c9828a0.

* Remove sources for other targets, include sources for main bundle

* Apply suggestion from @mozzius

* Apply suggestion from @mozzius

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2025-10-22 09:17:52 -05:00
Alex Benzer 6086bc5d5c Add a client event for "See more" link on suggested user interstitials (#9249) 2025-10-22 03:53:42 -07:00
Samuel Newman 199821d670 make gifs square again (#9236) 2025-10-20 07:15:01 -07:00
Eric Bailey 11dff765db Hook up post shadow to saved posts (#9225) 2025-10-20 03:05:49 -07:00
hailey 73639aa571 fix regex (#9234) 2025-10-19 09:20:53 -07:00
pfrazee f3b6d85913 Nightly source-language update 2025-10-17 02:36:59 +00:00
Samuel Newman 239d3cc535 [Release prep] 1.109.0 translations (#9218)
* intl:pull

* intl:extract:all
2025-10-16 10:08:15 -07:00
Samuel Newman d49d43f6eb update app icon (#9219) 2025-10-16 10:08:06 -07:00
Samuel Newman a482dae936 [Release prep] 1.109.0 E2E fixes (#9216)
* list tests

* fix create account

* get onboarding e2e tests working

* get curate list test working

* Update thread-muting.yml
2025-10-16 07:31:11 -07:00
Samuel Newman 25d44d8939 update xcode (#9215) 2025-10-16 07:26:52 -07:00
pfrazee e7801a0b76 Nightly source-language update 2025-10-16 02:37:46 +00:00
Eric Bailey 7861cd153d Remove notice in create account flow (#9209)
* Remove notice of policy updates

* Rm debug code
2025-10-15 13:56:29 -05:00
Samuel Newman be53a168ba Move interests clientside and add finance (#9176)
* move interests clientside

* add finance

* try and ensure there's always data for the "all" category

* add empty state for suggested accounts

* simplify error wording
2025-10-15 09:37:24 -07:00
Samuel Newman b56d0c419b add sort import suggestions plugin (#8335) 2025-10-15 09:37:07 -07:00
Samuel Newman 03adb4260f patch fractional font size issue (#9199) 2025-10-15 09:14:42 -07:00
Eric Bailey 27926f093c Fix computation of isLastSibling, isLastChild, and replyIndex (#9202)
* Fix computation of isLastSibling and isLastChild to account for muted or
otherwise hidden replies

* Update comments

* isLastSiblingByCounts isn't needed, should rely only on the count of replies seen

* The counters serve the same purpose, we only need to know the count of the actual replies rendered to the view in order to calculate the replyIndex

* Remove redundant check
2025-10-15 11:13:08 -05:00
Samuel Newman fe5a623b44 Haptic on profile buttons (#6034)
* play haptic on profile buttons

* change react import
2025-10-15 09:10:26 -07:00
Samuel Newman fc944841ce add large content viewer to tabs (#8394) 2025-10-15 11:02:47 -05:00
Samuel Newman 66ca3e34a4 fix invalid tweaking to date string (#8415) 2025-10-15 11:00:29 -05:00
Samuel Newman 4788106f32 change flatlist config for explore page (#9083) 2025-10-15 08:57:30 -07:00
Samuel Newman 6414bda252 delay toast after posting (#9208) 2025-10-15 08:56:35 -07:00
Samuel Newman 591fd262b3 make sure border radius matches inset border (#9207) 2025-10-15 08:55:46 -07:00
Samuel Newman ede10368d5 cache pages in explore feed previews (#9082) 2025-10-15 18:54:54 +03:00
Samuel Newman 1127e50248 [Onboarding] Add starterpacks step (#9007)
* add onboarding starterpacks step

* tweak starter pack cards

* fix web styling

* use new API

* add screen transitions to onboarding

* skip host account

* bump limit to 6

* tweak font weight

* rm commented out code

* fix yarn.lock

* limit starter packs experiment to english speakers
2025-10-15 07:32:38 -07:00
jim e4485da4ff Merge pull request #9189 from internet-development/binaryfiddler/hashtag
prompt user to log in on hashtag screen
2025-10-15 01:12:49 -07:00
Alex Benzer 6c7fd96528 Add metrics for when OPs click "hide reply for me" or "hide reply for everyone" (#9206) 2025-10-15 08:33:30 +01:00
pfrazee 6a1c0ff346 Nightly source-language update 2025-10-15 02:38:20 +00:00
Chenyu Huang ad18439148 use hasSession 2025-10-14 14:15:19 -07:00
Eric Bailey 7280e44ea2 Remove log (#9204) 2025-10-14 11:49:02 -05:00
Eric Bailey 7e6b23bd86 Fix ternary in moderation settings that caused a double title (#9198) 2025-10-14 09:17:34 -05:00
pfrazee d2bc22e9be Nightly source-language update 2025-10-14 02:36:22 +00:00
Eric Bailey 0b50e9f109 Remove remaining usages of old post thread query (#9184)
* Remove remaining usages of old post thread query

* Add PostThreadContext, cache mutator for threadgates on threads, pipe it through

* Replace getPostThread in threadgate query

* Replace in initQuote handling, which isn't even used rn...

* Missing import

* Revert ext change
2025-10-13 18:36:11 -05:00
Samuel Newman 0f6a99c8a5 enable numberOfLines on replyTo component (#9196) 2025-10-13 09:36:06 -07:00
Samuel Newman ea9986f127 fix label spacing (#9195) 2025-10-13 09:35:31 -07:00
Samuel Newman 5222a6e1c3 tidy up post reasons (#9194) 2025-10-13 08:24:02 -07:00
pfrazee b7f505ab00 Nightly source-language update 2025-10-11 02:32:55 +00:00
Chenyu Huang 34f2cc2322 prompt user to log in on hashtag screen 2025-10-10 14:17:57 -07:00
Eric Bailey 8a30131686 Combine SubtleWebHover and SubtleHover, default to web only for all cases (#9188) 2025-10-10 15:05:20 -05:00
227 changed files with 70737 additions and 69837 deletions
+10 -5
View File
@@ -50,8 +50,8 @@ jobs:
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
distribution: "temurin"
java-version: "17"
- name: ⚙️ Install dependencies
run: yarn install
@@ -96,7 +96,7 @@ jobs:
id: timestamp
uses: nanzm/get-time-action@master
with:
format: 'MM-DD-HH-mm-ss'
format: "MM-DD-HH-mm-ss"
- name: 🚀 Upload Production Artifact
id: upload-artifact-production
@@ -146,6 +146,11 @@ jobs:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: 🧹 Clear Metro cache
if: ${{ inputs.profile == 'production' }}
# https://github.com/expo/eas-cli/issues/2959#issuecomment-2749791326
run: rm -rf ${TMPDIR:-/tmp}/metro-cache
- name: 🏗️ Build Production APK
if: ${{ inputs.profile == 'production' }}
run: >
@@ -182,11 +187,11 @@ jobs:
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
if: ${{ inputs.profile == 'testflight' }}
if: ${{ inputs.profile == 'testflight-android' }}
with:
path: most-recent-testflight-commit.txt
key: most-recent-testflight-commit
- name: ✏️ Write commit hash to cache
if: ${{ inputs.profile == 'testflight' }}
if: ${{ inputs.profile == 'testflight-android' }}
run: echo ${{ github.sha }} > most-recent-testflight-commit.txt
+23 -4
View File
@@ -15,7 +15,7 @@ jobs:
build:
if: github.repository == 'bluesky-social/social-app'
name: Build and Submit iOS
runs-on: macos-15
runs-on: macos-26
steps:
- name: Check for EXPO_TOKEN
run: >
@@ -53,7 +53,7 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "16.4"
xcode-version: "26.0"
- name: ☕️ Setup Cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
@@ -98,10 +98,29 @@ jobs:
yarn use-build-number-with-bump
eas build -p ios
--profile ${{ inputs.profile || 'testflight' }}
--local --output build.ipa --non-interactive
--local --output build.tar.gz --non-interactive
- name: 📂 Extract build artifact
run: |
if [ -f "build.tar.gz" ]; then
echo "Extracting build.tar.gz..."
mkdir ios-build
tar -xzf build.tar.gz -C ios-build
echo "Extraction completed successfully"
else
echo "Archive file not found!"
exit 1
fi
- name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path build.ipa
run: eas submit -p ios --non-interactive --path ios-build/ios/build/Bluesky.ipa
- name: 🪲 Upload dSYM to Sentry
run: >
SENTRY_ORG=blueskyweb
SENTRY_PROJECT=app
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
yarn sentry-cli debug-files upload ios-build/ios/build/Bluesky.app.dSYM.zip --include-sources
- name: 📚 Get version from package.json
id: get-build-info
@@ -150,7 +150,7 @@ jobs:
# GitHub actions are horrible so let's just copy paste this in
buildIfNecessaryIOS:
name: Build and Submit iOS
runs-on: macos-15
runs-on: macos-26
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
cancel-in-progress: true
@@ -192,7 +192,7 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "16.2"
xcode-version: "26.0"
- name: ☕️ Setup Cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
+5
View File
@@ -119,3 +119,8 @@ src/locale/locales/**/*.js
# ogcard assets
bskyogcard/src/assets/fonts/noto-*
# bskyweb build output
bskyweb/static/media/*.webp
bskyweb/static/media/*.jpg
bskyweb/static/media/*.png
+6 -3
View File
@@ -3,7 +3,7 @@ appId: xyz.blueskyweb.app
- runScript:
file: ../setupServer.js
env:
SERVER_PATH: ""
SERVER_PATH: ""
- runFlow:
file: ../setupApp.yml
- tapOn:
@@ -25,7 +25,10 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "passwordInput"
- inputText: "hunter22"
- hideKeyboard
- tapOn:
# hideKeyboard on ios is borked
# https://docs.maestro.dev/troubleshooting/known-issues#hidekeyboard-command-is-flaky
text: "Your account"
- tapOn:
id: "nextBtn"
- tapOn:
@@ -34,7 +37,7 @@ appId: xyz.blueskyweb.app
- inputText: "e2e-test"
- extendedWaitUntil:
visible:
id: "handleAvailableCheck"
id: "handleAvailableCheck"
- tapOn:
id: "nextBtn"
- assertVisible: "Give your profile a face"
+19 -33
View File
@@ -17,63 +17,52 @@ appId: xyz.blueskyweb.app
id: "e2eGotoLists"
- tapOn:
id: "newUserListBtn"
- assertVisible:
id: "createOrEditListModal"
- waitForAnimationToEnd
- assertVisible: "Create user list"
- tapOn:
id: "editNameInput"
id: "editListNameInput"
- inputText: "Good Ppl"
- tapOn:
id: "editDescriptionInput"
id: "editListDescriptionInput"
- inputText: "They good"
- tapOn: "Save"
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- assertNotVisible: "Create user list"
- tapOn: "People"
- assertVisible: "Good Ppl"
- assertVisible: "They good"
- tapOn:
label: "Edit display name and description via the edit curatelist modal"
point: "90%,9%"
id: "moreOptionsBtn"
- tapOn: "Edit list details"
- assertVisible:
id: "createOrEditListModal"
- assertVisible: "Edit user list"
- tapOn:
id: "editNameInput"
id: "editListNameInput"
- eraseText
- inputText: "Bad Ppl"
- hideKeyboard
- tapOn:
id: "editDescriptionInput"
id: "editListDescriptionInput"
- eraseText
- inputText: "They bad"
- tapOn: "Save"
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- assertNotVisible: "Edit user list"
- assertVisible: Bad Ppl
- assertVisible: They bad
- tapOn:
label: "Remove description via the edit curatelist modal"
point: "90%,9%"
id: "moreOptionsBtn"
- tapOn: "Edit list details"
- assertVisible:
id: "createOrEditListModal"
- assertVisible: "Edit user list"
- tapOn:
id: "editDescriptionInput"
id: "editListDescriptionInput"
- eraseText
- tapOn: "Save"
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- assertNotVisible: "Edit user list"
- assertNotVisible:
id: "listDescription"
- tapOn:
label: "Delete the curatelist"
point: "90%,9%"
id: "moreOptionsBtn"
- tapOn: "Delete List"
- tapOn:
id: "confirmBtn"
@@ -82,18 +71,15 @@ appId: xyz.blueskyweb.app
id: "newUserListBtn"
- tapOn:
id: "newUserListBtn"
- assertVisible:
id: "createOrEditListModal"
- assertVisible: "Create user list"
- tapOn:
id: "editNameInput"
id: "editListNameInput"
- inputText: "Good Ppl"
- tapOn:
id: "editDescriptionInput"
id: "editListDescriptionInput"
- inputText: "They good"
- tapOn: "Save"
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- assertNotVisible: "Create user list"
- tapOn: "People"
- assertVisible: "Good Ppl"
- assertVisible: "They good"
+2 -6
View File
@@ -19,21 +19,17 @@ appId: xyz.blueskyweb.app
id: "moderationlistsBtn"
- tapOn: "New list"
- tapOn:
id: "editNameInput"
id: "editListNameInput"
- inputText: "Muted Users"
- tapOn:
id: "editDescriptionInput"
id: "editListDescriptionInput"
- inputText: "Shhh"
- tapOn: "Save"
- tapOn: "Save"
# view modlist
- assertVisible: "Muted Users"
- assertVisible: "Shhh"
# DOES NOT WORK - THE BUTTON IS NOT ACCESSIBLE
# IGNORING FOR NOW, FIX THE COMPONENTS IN THE NEXT RELEASE
# BECAUSE THIS IS A LEGIT A11Y PROBLEM -sfn
- tapOn: "Subscribe to this list"
- tapOn: "Mute accounts"
- tapOn: "Mute list"
+8 -1
View File
@@ -30,7 +30,14 @@ appId: xyz.blueskyweb.app
- assertVisible: "What are your interests?"
- tapOn:
id: "onboardingContinue"
- assertVisible: "You're ready to go!"
- assertVisible: "Suggested for you"
# mock server doesn't have suggestions api
- tapOn: "Skip to next step"
- swipe:
direction: "LEFT"
- swipe:
direction: "LEFT"
- assertVisible: "Complete onboarding and start using your account"
- tapOn:
id: "onboardingFinish"
- tapOn:
+10 -2
View File
@@ -16,7 +16,8 @@ appId: xyz.blueskyweb.app
id: "e2eStartOnboarding"
- tapOn: "Select an avatar"
- waitForAnimationToEnd
- assertVisible: "Search your library…"
- assertVisible: "Photos"
- assertVisible: "Collections"
- tapOn:
point: "50%,22%"
- waitForAnimationToEnd
@@ -27,7 +28,14 @@ appId: xyz.blueskyweb.app
- assertVisible: "What are your interests?"
- tapOn:
id: "onboardingContinue"
- assertVisible: "You're ready to go!"
- assertVisible: "Suggested for you"
# mock server doesn't have suggestions api
- tapOn: "Skip to next step"
- swipe:
direction: "LEFT"
- swipe:
direction: "LEFT"
- assertVisible: "Complete onboarding and start using your account"
- tapOn:
id: "onboardingFinish"
- tapOn:
+4
View File
@@ -31,6 +31,8 @@ appId: xyz.blueskyweb.app
id: "viewHeaderHomeFeedPrefsBtn"
- tapOn:
id: "replyBtn"
- tapOn:
id: "replyBtn"
- inputText: "Reply 1"
- tapOn:
id: "composerPublishBtn"
@@ -69,6 +71,8 @@ appId: xyz.blueskyweb.app
id: "profilePager-selector-1"
- tapOn:
id: "replyBtn"
- tapOn:
id: "replyBtn"
- inputText: "Reply 2"
- tapOn:
id: "composerPublishBtn"
+6 -6
View File
@@ -1,18 +1,18 @@
appId: xyz.blueskyweb.app
---
- launchApp:
appId: "xyz.blueskyweb.app"
clearState: true
appId: "xyz.blueskyweb.app"
clearState: true
- waitForAnimationToEnd
- tapOn: "http://localhost:8081"
- waitForAnimationToEnd
- extendedWaitUntil:
visible: "Continue"
- swipe:
from: "Bluesky"
direction: DOWN
from: "Bluesky"
direction: DOWN
duration: 100
- tapOn:
id: e2eProxyHeaderInput
id: e2eProxyHeaderInput
- inputText: ${output.result}
- pressKey: Enter
- hideKeyboard
+6
View File
@@ -378,6 +378,7 @@ describe('parseEmbedPlayerFromUrl', () => {
'https://music.apple.com/us/playlist/playlistName/playlistId',
'https://music.apple.com/us/album/albumName/albumId',
'https://music.apple.com/us/album/albumName/albumId?i=songId',
'https://music.apple.com/us/song/songName/songId',
'https://vimeo.com/videoId',
'https://vimeo.com/videoId?autoplay=0',
@@ -604,6 +605,11 @@ describe('parseEmbedPlayerFromUrl', () => {
playerUri:
'https://embed.music.apple.com/us/album/albumName/albumId?i=songId',
},
{
type: 'apple_music_song',
source: 'appleMusic',
playerUri: 'https://embed.music.apple.com/us/song/songName/songId',
},
{
type: 'vimeo_video',
+41 -15
View File
@@ -11,7 +11,7 @@ module.exports = function (_config) {
*
* @see https://docs.expo.dev/build-reference/variables/#built-in-environment-variables
*/
const PLATFORM = process.env.EAS_BUILD_PLATFORM
const PLATFORM = process.env.EAS_BUILD_PLATFORM ?? 'web'
const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
const IS_PRODUCTION = process.env.EXPO_PUBLIC_ENV === 'production'
@@ -40,7 +40,7 @@ module.exports = function (_config) {
runtimeVersion: {
policy: 'appVersion',
},
icon: './assets/app-icons/ios_icon_default_light.png',
icon: './assets/app-icons/ios_icon_default_next.png',
userInterfaceStyle: 'automatic',
primaryColor: '#1083fe',
newArchEnabled: false,
@@ -50,6 +50,10 @@ module.exports = function (_config) {
config: {
usesNonExemptEncryption: false,
},
icon:
PLATFORM === 'web' // web build doesn't like .icon files
? './assets/app-icons/ios_icon_default_next.png'
: './assets/app-icons/ios_icon_default.icon',
infoPlist: {
UIBackgroundModes: ['remote-notification'],
NSCameraUsageDescription:
@@ -112,6 +116,34 @@ module.exports = function (_config) {
'com.apple.security.application-groups': 'group.app.bsky',
},
privacyManifests: {
NSPrivacyCollectedDataTypes: [
{
NSPrivacyCollectedDataType: 'NSPrivacyCollectedDataTypeCrashData',
NSPrivacyCollectedDataTypeLinked: false,
NSPrivacyCollectedDataTypeTracking: false,
NSPrivacyCollectedDataTypePurposes: [
'NSPrivacyCollectedDataTypePurposeAppFunctionality',
],
},
{
NSPrivacyCollectedDataType:
'NSPrivacyCollectedDataTypePerformanceData',
NSPrivacyCollectedDataTypeLinked: false,
NSPrivacyCollectedDataTypeTracking: false,
NSPrivacyCollectedDataTypePurposes: [
'NSPrivacyCollectedDataTypePurposeAppFunctionality',
],
},
{
NSPrivacyCollectedDataType:
'NSPrivacyCollectedDataTypeOtherDiagnosticData',
NSPrivacyCollectedDataTypeLinked: false,
NSPrivacyCollectedDataTypeTracking: false,
NSPrivacyCollectedDataTypePurposes: [
'NSPrivacyCollectedDataTypePurposeAppFunctionality',
],
},
],
NSPrivacyAccessedAPITypes: [
{
NSPrivacyAccessedAPIType:
@@ -143,12 +175,11 @@ module.exports = function (_config) {
barStyle: 'light-content',
},
android: {
icon: './assets/app-icons/android_icon_default_light.png',
icon: './assets/app-icons/android_icon_default_next.png',
adaptiveIcon: {
foregroundImage: './assets/icon-android-foreground.png',
monochromeImage: './assets/icon-android-foreground.png',
backgroundImage: './assets/icon-android-background.png',
backgroundColor: '#1185FE',
monochromeImage: './assets/icon-android-monochrome.png',
backgroundColor: '#006AFF',
},
googleServicesFile: './google-services.json',
package: 'xyz.blueskyweb.app',
@@ -295,18 +326,13 @@ module.exports = function (_config) {
* Default set
*/
default_light: {
ios: './assets/app-icons/ios_icon_default_light.png',
android: './assets/app-icons/android_icon_default_light.png',
ios: './assets/app-icons/ios_icon_legacy_light.png',
android: './assets/app-icons/android_icon_legacy_light.png',
prerendered: true,
},
default_dark: {
ios: './assets/app-icons/ios_icon_default_dark.png',
android: './assets/app-icons/android_icon_default_dark.png',
prerendered: true,
},
next: {
ios: './assets/app-icons/icon_default_next.png',
android: './assets/app-icons/icon_default_next.png',
ios: './assets/app-icons/ios_icon_legacy_dark.png',
android: './assets/app-icons/android_icon_legacy_dark.png',
prerendered: true,
},
Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Before

Width:  |  Height:  |  Size: 467 KiB

After

Width:  |  Height:  |  Size: 467 KiB

Before

Width:  |  Height:  |  Size: 432 KiB

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 KiB

@@ -0,0 +1,31 @@
{
"fill" : {
"automatic-gradient" : "srgb:0.00000,0.41569,1.00000,1.00000"
},
"groups" : [
{
"layers" : [
{
"fill" : "none",
"glass" : false,
"image-name" : "iOS transparent.png",
"name" : "iOS transparent"
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

Before

Width:  |  Height:  |  Size: 389 KiB

After

Width:  |  Height:  |  Size: 389 KiB

Before

Width:  |  Height:  |  Size: 321 KiB

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg width="105" height="32" viewBox="0 0 105 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.7901 9.77492C21.7947 11.2889 23.9508 14.3587 24.7425 16.0059C25.5342 14.3587 27.6903 11.2889 29.6949 9.77492C31.1413 8.68251 33.485 7.83725 33.485 10.5269C33.485 11.064 33.1789 15.0393 32.9993 15.6848C32.3752 17.9285 30.1009 18.5008 28.0778 18.1544C31.6141 18.7598 32.5136 20.7653 30.5709 22.7708C26.1996 27.2831 24.7425 20.3569 24.7425 20.3569C24.7425 20.3569 23.2854 27.2831 18.9142 22.7708C16.9714 20.7653 17.871 18.7598 21.4072 18.1544C19.3841 18.5008 17.1099 17.9285 16.4857 15.6848C16.3061 15.0393 16 11.064 16 10.5269C16 7.83725 18.3437 8.68251 19.7901 9.77492Z" fill="white"/>
<path d="M44.3863 16.3646C45.5901 16.7932 46.2292 17.7837 46.2292 18.8924C46.2292 20.7698 44.9659 21.908 42.5434 21.908H37.5942V11.3828H42.3799C44.6835 11.3828 45.7982 12.5506 45.7982 14.1028C45.7982 15.1376 45.3226 15.8915 44.3863 16.3646ZM42.2313 13.0237H39.6006V15.7732H42.2313C43.2568 15.7732 43.8067 15.2411 43.8067 14.3541C43.8067 13.5411 43.2419 13.0237 42.2313 13.0237ZM39.6006 20.2524H42.4393C43.5689 20.2524 44.1782 19.735 44.1782 18.8037C44.1782 17.828 43.5986 17.3402 42.4393 17.3402H39.6006V20.2524Z" fill="white"/>
<path d="M49.0111 21.908H47.1385V11.3828H49.0111V21.908Z" fill="white"/>
<path d="M55.2202 18.5524V14.2802H57.0929V21.908H55.2797V20.7993C54.7 21.6567 53.8975 22.0854 52.872 22.0854C51.252 22.0854 50.1968 21.1098 50.1968 19.3359V14.2802H52.0694V19.0254C52.0694 19.9863 52.545 20.4741 53.5111 20.4741C54.4177 20.4741 55.2202 19.8089 55.2202 18.5524Z" fill="white"/>
<path d="M65.6167 18.2272V18.6706H59.8799C60.0137 19.9863 60.7419 20.6367 61.8714 20.6367C62.7334 20.6367 63.3131 20.2672 63.6252 19.5428H65.4235C65.0222 21.095 63.6846 22.0854 61.8566 22.0854C60.7122 22.0854 59.7907 21.7159 59.0922 20.9915C58.3937 20.2672 58.037 19.3063 58.037 18.0941C58.037 16.8967 58.3788 15.9359 59.0773 15.1967C59.7759 14.4724 60.6825 14.1028 61.8269 14.1028C62.9861 14.1028 63.9076 14.4872 64.5912 15.2411C65.2749 15.995 65.6167 17.0002 65.6167 18.2272ZM61.812 15.5515C60.7865 15.5515 60.0731 16.1428 59.8948 17.3698H63.7441C63.5806 16.2611 62.8969 15.5515 61.812 15.5515Z" fill="white"/>
<path d="M69.7673 22.115C67.5379 22.115 66.3638 21.2428 66.2598 19.4837H68.0878C68.1918 20.4298 68.6674 20.7846 69.797 20.7846C70.8076 20.7846 71.3129 20.4741 71.3129 19.868C71.3129 19.3211 70.9562 19.055 69.8118 18.8628L68.935 18.715C67.2555 18.4341 66.4232 17.6654 66.4232 16.4089C66.4232 14.975 67.5676 14.1028 69.6038 14.1028C71.7885 14.1028 72.9181 14.9602 72.9924 16.6898H71.2238C71.1792 15.7585 70.6293 15.4332 69.6038 15.4332C68.712 15.4332 68.2662 15.7289 68.2662 16.3202C68.2662 16.8524 68.6526 17.0889 69.5443 17.2515L70.5104 17.3993C72.3681 17.7393 73.1707 18.4193 73.1707 19.7202C73.1707 21.2576 71.9223 22.115 69.7673 22.115Z" fill="white"/>
<path d="M81.3899 21.908H79.2497L77.0204 18.3602L75.8611 19.5132V21.908H74.0182V11.3828H75.8611V17.4437L78.9822 14.2802H81.2116L78.3134 17.1628L81.3899 21.908Z" fill="white"/>
<path d="M86.2805 16.2019L86.9047 14.2802H88.8665L85.909 22.5289C85.5968 23.3715 85.2104 23.9776 84.72 24.3176C84.2295 24.6576 83.531 24.8202 82.6095 24.8202C82.2974 24.8202 82.0299 24.8054 81.7921 24.7759V23.3124H82.5055C83.3526 23.3124 83.7688 22.795 83.7688 22.0854C83.7688 21.7306 83.6499 21.2132 83.4121 20.548L81.1827 14.2802H83.204L83.8282 16.1872C84.289 17.6211 84.6902 19.0402 85.0469 20.4446C85.3739 19.2324 85.7901 17.8132 86.2805 16.2019Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

+4 -2
View File
@@ -37,7 +37,7 @@ export function Container({
return (
<div
ref={ref}
className="w-full bg-white text-black hover:bg-neutral-50 dark:bg-dimmedBg dark:hover:bg-dimmedBgLighten relative transition-colors max-w-[600px] min-w-[300px] flex border dark:border-slate-600 dark:text-slate-200 rounded-xl"
className="w-full bg-brand text-black dark:bg-brand relative transition-colors max-w-[600px] min-w-[300px] flex items-center dark:text-slate-200 rounded-[20px] cursor-pointer hover:bg-opacity-90"
onClick={() => {
if (ref.current && href) {
// forwardRef requires preact/compat - let's keep it simple
@@ -49,7 +49,9 @@ export function Container({
}
}}>
{href && <Link href={href} />}
<div className="flex-1 px-4 pt-3 pb-2.5 max-w-full">{children}</div>
<div className="flex-1 px-[6px] pt-[6px] pb-2.5 max-w-full">
{children}
</div>
</div>
)
}
+3 -3
View File
@@ -83,9 +83,9 @@ export function Embed({
return (
<Link
href={`/profile/${record.author.did}/post/${getRkey(record)}`}
className="transition-colors hover:bg-neutral-100 dark:hover:bg-slate-700 border dark:border-slate-600 rounded-xl p-2 gap-1.5 w-full flex flex-col">
className="transition-colors hover:bg-blue-50 dark:hover:bg-slate-900 border dark:border-slate-600 rounded-xl p-2 gap-1.5 w-full flex flex-col">
<div className="flex gap-1.5 items-center">
<div className="w-4 h-4 rounded-full bg-neutral-300 dark:bg-slate-700 shrink-0">
<div className="w-4 h-4 rounded-full bg-neutral-300 dark:bg-slate-900 shrink-0">
<img
className="rounded-full"
src={record.author.avatar}
@@ -222,7 +222,7 @@ export function Embed({
function Info({children}: {children: ComponentChildren}) {
return (
<div className="w-full rounded-xl border py-2 px-2.5 flex-row flex gap-2 bg-neutral-50">
<div className="w-full rounded-xl border py-2 px-2.5 flex-row flex gap-2 hover:bg-blue-50 dark:border-slate-600 dark:hover:bg-slate-900">
<img src={infoIcon} className="w-4 h-4 shrink-0 mt-0.5" />
<p className="text-sm text-textLight dark:text-textDimmed">{children}</p>
</div>
+63 -51
View File
@@ -6,10 +6,10 @@ import {
} from '@atproto/api'
import {h} from 'preact'
import replyIcon from '../../assets/bubble_filled_stroke2_corner2_rounded.svg'
import likeIcon from '../../assets/heart2_filled_stroke2_corner0_rounded.svg'
import logo from '../../assets/logo.svg'
import repostIcon from '../../assets/repost_stroke2_corner2_rounded.svg'
import logo from '../../assets/logo_full_name.svg'
import {Like as LikeIcon} from '../icons/Like'
import {Reply as ReplyIcon} from '../icons/Reply'
import {Repost as RepostIcon} from '../icons/Repost'
import {CONTENT_LABELS} from '../labels'
import * as bsky from '../types/bsky'
import {niceDate} from '../util/nice-date'
@@ -45,10 +45,13 @@ export function Post({thread}: Props) {
const verification = getVerificationState({profile: post.author})
const href = `/profile/${post.author.did}/post/${getRkey(post)}`
return (
<Container href={href}>
<div className="flex-1 flex-col flex gap-2" lang={record?.langs?.[0]}>
<div className="flex gap-2.5 items-center cursor-pointer w-full max-w-full">
<div
className="flex-1 flex-col flex gap-2 bg-neutral-50 dark:bg-black dark:hover:bg-slate-900 hover:bg-blue-50 rounded-[14px] p-4"
lang={record?.langs?.[0]}>
<div className="flex gap-2.5 items-center cursor-pointer w-full max-w-full ">
<Link
href={`/profile/${post.author.did}`}
className="rounded-full shrink-0">
@@ -80,57 +83,66 @@ export function Post({thread}: Props) {
@{post.author.handle}
</Link>
</div>
<Link
href={href}
className="transition-transform hover:scale-110 shrink-0 self-start">
<img src={logo} className="h-8" />
</Link>
</div>
<PostContent record={record} />
<Embed content={post.embed} labels={post.labels} />
<Link href={href}>
<time
datetime={new Date(post.indexedAt).toISOString()}
className="text-textLight dark:text-textDimmed mt-1 text-sm hover:underline">
{niceDate(post.indexedAt)}
</time>
</Link>
<div className="border-t dark:border-slate-600 w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer">
{!!post.likeCount && (
<div className="flex items-center gap-2 cursor-pointer">
<img src={likeIcon} className="w-5 h-5" />
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
{prettyNumber(post.likeCount)}
</p>
</div>
)}
{!!post.repostCount && (
<div className="flex items-center gap-2 cursor-pointer">
<img src={repostIcon} className="w-5 h-5" />
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
{prettyNumber(post.repostCount)}
</p>
</div>
)}
<div className="flex items-center gap-2 cursor-pointer">
<img src={replyIcon} className="w-5 h-5" />
<p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px">
Reply
</p>
<div className="flex items-center justify-between w-full pt-2.5 text-sm">
<div className="flex items-center gap-3 text-sm cursor-pointer">
{!!post.likeCount && (
<div className="flex items-center gap-1 cursor-pointer group">
<LikeIcon
width={20}
height={20}
className="text-slate-600 dark:text-slate-400 group-hover:text-neutral-800 dark:group-hover:text-white transition-colors"
/>
<p className="font-medium text-slate-600 text-neutral-600 dark:text-neutral-300 mb-px group-hover:text-neutral-800 dark:group-hover:text-white transition-colors dark:text-slate-400">
{prettyNumber(post.likeCount)}
</p>
</div>
)}
{!!post.replyCount && (
<div className="flex items-center gap-1 cursor-pointer group">
<ReplyIcon
width={20}
height={20}
className="text-slate-600 dark:text-slate-400 group-hover:text-neutral-800 dark:group-hover:text-white transition-colors"
/>
<p className="font-medium text-slate-600 text-neutral-600 dark:text-neutral-300 mb-px group-hover:text-neutral-800 dark:group-hover:text-white transition-colors dark:text-slate-400">
{prettyNumber(post.replyCount)}
</p>
</div>
)}
{!!post.repostCount && (
<div className="flex items-center gap-1 cursor-pointer group">
<RepostIcon
width={20}
height={20}
className="text-slate-600 dark:text-slate-400 group-hover:text-neutral-800 dark:group-hover:text-white transition-colors"
/>
<p className="font-medium text-slate-600 dark:text-slate-400 mb-px group-hover:text-neutral-800 dark:group-hover:text-white transition-colors">
{prettyNumber(post.repostCount)}
</p>
</div>
)}
</div>
<div className="flex-1" />
<p className="cursor-pointer text-brand dark:text-brandLighten font-bold hover:underline hidden min-[450px]:inline">
{post.replyCount
? `Read ${prettyNumber(post.replyCount)} ${
post.replyCount > 1 ? 'replies' : 'reply'
} on Bluesky`
: `View on Bluesky`}
</p>
<p className="cursor-pointer text-brand font-bold hover:underline min-[450px]:hidden">
<span className="hidden min-[380px]:inline">View on </span>Bluesky
</p>
<Link href={href}>
<time
datetime={new Date(post.indexedAt).toISOString()}
className="text-slate-500 dark:text-textDimmed text-sm hover:underline dark:text-slate-500">
{niceDate(post.indexedAt)}
</time>
</Link>
</div>
</div>
<div className="flex items-center justify-end pt-2">
<Link
href={href}
className="transition-transform hover:scale-110 shrink-0">
<img src={logo} className="h-8" />
</Link>
</div>
</Container>
)
}
+28
View File
@@ -0,0 +1,28 @@
import {h} from 'preact'
export const Like = ({
width = 16,
height = 17,
fill = 'currentColor',
className,
}: {
width?: number
height?: number
fill?: string
className?: string
}) => (
<svg
className={className}
width={width}
height={height}
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11.1561 3.62664C10.3307 3.44261 9.35086 3.65762 8.47486 4.54615C8.34958 4.67323 8.17857 4.74478 8.00012 4.74478C7.82167 4.74478 7.65066 4.67324 7.52538 4.54616C6.64938 3.65762 5.66955 3.44261 4.84416 3.62664C4.0022 3.81438 3.25812 4.43047 2.89709 5.33069C2.21997 7.01907 2.83524 10.1257 8.00015 13.1315C13.165 10.1257 13.7803 7.01906 13.1032 5.33069C12.7421 4.43047 11.998 3.81437 11.1561 3.62664ZM14.3407 4.83438C15.4101 7.50098 14.0114 11.2942 8.32611 14.4808C8.12362 14.5943 7.87668 14.5943 7.6742 14.4808C1.98891 11.2942 0.590133 7.501 1.65956 4.83439C2.1788 3.53968 3.26862 2.61187 4.55399 2.32527C5.68567 2.07294 6.92237 2.32723 8.00012 3.18278C9.07786 2.32723 10.3146 2.07294 11.4462 2.32526C12.7316 2.61186 13.8214 3.53967 14.3407 4.83438Z"
fill={fill}
/>
</svg>
)
+28
View File
@@ -0,0 +1,28 @@
import {h} from 'preact'
export const Reply = ({
width = 16,
height = 17,
fill = 'currentColor',
className,
}: {
width?: number
height?: number
fill?: string
className?: string
}) => (
<svg
className={className}
width={width}
height={height}
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.3335 4.23242C1.3335 3.12785 2.22893 2.23242 3.3335 2.23242H12.6668C13.7714 2.23242 14.6668 3.12785 14.6668 4.23242V10.8991C14.6668 12.0037 13.7714 12.8991 12.6668 12.8991H8.18482L5.00983 14.8041C4.80387 14.9277 4.54737 14.9309 4.33836 14.8126C4.12936 14.6942 4.00016 14.4726 4.00016 14.2324V12.8991H3.3335C2.22893 12.8991 1.3335 12.0037 1.3335 10.8991V4.23242ZM3.3335 3.56576C2.96531 3.56576 2.66683 3.86423 2.66683 4.23242V10.8991C2.66683 11.2673 2.96531 11.5658 3.3335 11.5658H4.66683C5.03502 11.5658 5.3335 11.8642 5.3335 12.2324V13.055L7.65717 11.6608C7.76078 11.5986 7.87933 11.5658 8.00016 11.5658H12.6668C13.035 11.5658 13.3335 11.2673 13.3335 10.8991V4.23242C13.3335 3.86423 13.035 3.56576 12.6668 3.56576H3.3335Z"
fill={fill}
/>
</svg>
)
+26
View File
@@ -0,0 +1,26 @@
import {h} from 'preact'
export const Repost = ({
width = 16,
height = 17,
fill = 'currentColor',
className,
}: {
width?: number
height?: number
fill?: string
className?: string
}) => (
<svg
className={className}
width={width}
height={height}
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M3.86204 9.76164C4.12239 9.50134 4.54442 9.50131 4.80475 9.76164C5.06503 10.022 5.06503 10.444 4.80475 10.7044L3.94277 11.5663H11.3334C12.0697 11.5663 12.6667 10.9693 12.6667 10.233V8.89966C12.6667 8.53147 12.9652 8.233 13.3334 8.233C13.7015 8.23305 14.0001 8.53151 14.0001 8.89966V10.233C14.0001 11.7057 12.8061 12.8996 11.3334 12.8997H3.94277L4.80475 13.7616C5.06503 14.022 5.06503 14.444 4.80475 14.7044C4.54442 14.9647 4.12239 14.9646 3.86204 14.7044L2.3334 13.1757C1.8127 12.655 1.8127 11.811 2.3334 11.2903L3.86204 9.76164ZM2.00006 7.56633V6.233C2.00006 4.76024 3.19397 3.56633 4.66673 3.56633H12.0574L11.1954 2.70435C10.935 2.444 10.935 2.02199 11.1954 1.76164C11.4557 1.50134 11.8778 1.50131 12.1381 1.76164L13.6667 3.29029C14.1873 3.81096 14.1873 4.65503 13.6667 5.17571L12.1381 6.70435C11.8778 6.96468 11.4557 6.96465 11.1954 6.70435C10.935 6.444 10.935 6.02199 11.1954 5.76164L12.0574 4.89966H4.66673C3.93035 4.89966 3.3334 5.49662 3.3334 6.233V7.56633C3.3334 7.93449 3.03487 8.23294 2.66673 8.233C2.29854 8.233 2.00006 7.93452 2.00006 7.56633Z"
fill={fill}
/>
</svg>
)
+1 -1
View File
@@ -15,7 +15,7 @@ import {redirectLogger} from '../logger.js'
const SAFELINK_MIN_FETCH_INTERVAL = 1_000
const SAFELINK_MAX_FETCH_INTERVAL = 10_000
const SCHEME_REGEX = /^[a-zA-Z][a-zA-Z0-9+.-]*:/
const SCHEME_REGEX = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//
export class SafelinkClient {
private domainCache: LRUCache<string, SafelinkRule | 'ok'>
+1 -1
View File
@@ -75,7 +75,7 @@ After you do `yarn ios` and `yarn android` once, you can later just run `yarn we
- Start in various console tabs:
- `yarn e2e:mock-server`
- `yarn e2e:metro`
- `yarn e2e:start`
- Run once: `yarn e2e:build`
- Each test run: `yarn e2e:run`
+1 -1
View File
@@ -12,7 +12,7 @@ values.
### Running Maestro tests
- In one tab, run `yarn e2e:mock-server`
- In a second tab, run `yarn e2e:metro`
- In a second tab, run `yarn e2e:build`
- In a third tab, run `yarn e2e:run`
## Using Flashlight for Performance Testing
+8 -4
View File
@@ -36,7 +36,8 @@
"extends": "base",
"ios": {
"resourceClass": "large",
"autoIncrement": true
"autoIncrement": true,
"buildArtifactPaths": ["ios/build/*"]
},
"android": {
"autoIncrement": true
@@ -50,7 +51,8 @@
"extends": "base",
"distribution": "internal",
"ios": {
"autoIncrement": false
"autoIncrement": false,
"buildArtifactPaths": ["ios/build/*"]
},
"android": {
"autoIncrement": false
@@ -63,7 +65,8 @@
"testflight": {
"extends": "base",
"ios": {
"autoIncrement": true
"autoIncrement": true,
"buildArtifactPaths": ["ios/build/*"]
},
"android": {
"autoIncrement": true
@@ -77,7 +80,8 @@
"extends": "base",
"distribution": "internal",
"ios": {
"autoIncrement": true
"autoIncrement": true,
"buildArtifactPaths": ["ios/build/*"]
},
"android": {
"autoIncrement": true
+28 -26
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.109.0",
"version": "1.110.0",
"private": true,
"engines": {
"node": ">=20"
@@ -46,8 +46,9 @@
"lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules",
"typecheck": "tsc --project ./tsconfig.check.json",
"e2e:mock-server": "NODE_ENV=development ./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts",
"e2e:metro": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
"e2e:metro-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android",
"e2e:build": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
"e2e:build-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android",
"e2e:start": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo start",
"e2e:run": "maestro test __e2e__",
"perf:test": "NODE_ENV=test maestro test",
"perf:test:run": "NODE_ENV=test maestro test __e2e__/perf-test.yml",
@@ -71,7 +72,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
"@atproto/api": "^0.17.0",
"@atproto/api": "^0.17.6",
"@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2",
"@bsky.app/alf": "^0.1.5",
@@ -130,18 +131,18 @@
"emoji-mart": "^5.5.2",
"emoji-regex": "^10.4.0",
"eventemitter3": "^5.0.1",
"expo": "^54.0.9",
"expo": "^54.0.22",
"expo-application": "~7.0.7",
"expo-blur": "~15.0.7",
"expo-build-properties": "~1.0.9",
"expo-camera": "~17.0.8",
"expo-camera": "~17.0.9",
"expo-clipboard": "~8.0.7",
"expo-dev-client": "~6.0.12",
"expo-device": "~8.0.8",
"expo-file-system": "~19.0.14",
"expo-font": "~14.0.8",
"expo-dev-client": "~6.0.16",
"expo-device": "~8.0.9",
"expo-file-system": "~19.0.17",
"expo-font": "~14.0.9",
"expo-haptics": "~15.0.7",
"expo-image": "~3.0.8",
"expo-image": "~3.0.10",
"expo-image-crop-tool": "^0.1.8",
"expo-image-manipulator": "~14.0.7",
"expo-image-picker": "~17.0.8",
@@ -152,15 +153,15 @@
"expo-localization": "~17.0.7",
"expo-location": "~19.0.7",
"expo-media-library": "~18.2.0",
"expo-notifications": "~0.32.11",
"expo-notifications": "~0.32.12",
"expo-screen-orientation": "~9.0.7",
"expo-sharing": "~14.0.7",
"expo-splash-screen": "~31.0.10",
"expo-system-ui": "~6.0.7",
"expo-task-manager": "~14.0.7",
"expo-updates": "~29.0.11",
"expo-video": "~3.0.11",
"expo-web-browser": "~15.0.7",
"expo-system-ui": "~6.0.8",
"expo-task-manager": "~14.0.8",
"expo-updates": "~29.0.12",
"expo-video": "~3.0.13",
"expo-web-browser": "~15.0.9",
"fast-text-encoding": "^1.0.6",
"history": "^5.3.0",
"hls.js": "^1.6.2",
@@ -185,7 +186,7 @@
"react-image-crop": "^11.0.7",
"react-is": "19",
"react-keyed-flatten-children": "^5.0.0",
"react-native": "0.81.4",
"react-native": "0.81.5",
"react-native-compressor": "^1.13.0",
"react-native-date-picker": "^5.0.13",
"react-native-device-attest": "^0.1.6",
@@ -220,7 +221,7 @@
"zod": "^3.20.2"
},
"devDependencies": {
"@atproto/dev-env": "^0.3.172",
"@atproto/dev-env": "^0.3.181",
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@babel/runtime": "^7.26.0",
@@ -228,13 +229,13 @@
"@lingui/cli": "^4.14.1",
"@lingui/macro": "^4.14.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@react-native/babel-preset": "0.81.4",
"@react-native/eslint-config": "^0.81.4",
"@react-native/typescript-config": "^0.81.4",
"@react-native/babel-preset": "0.81.5",
"@react-native/eslint-config": "^0.81.5",
"@react-native/typescript-config": "^0.81.5",
"@sentry/webpack-plugin": "^3.2.2",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react-native": "^13.2.0",
"@types/jest": "^29.4.0",
"@types/jest": "29.5.14",
"@types/lodash.chunk": "^4.2.7",
"@types/lodash.debounce": "^4.0.7",
"@types/lodash.isequal": "^4.5.6",
@@ -262,7 +263,7 @@
"husky": "^8.0.3",
"is-ci": "^3.0.1",
"jest": "^29.7.0",
"jest-expo": "~54.0.12",
"jest-expo": "~54.0.13",
"jest-junit": "^16.0.0",
"lint-staged": "^13.2.3",
"lockfile-lint": "^4.14.0",
@@ -272,12 +273,13 @@
"react-refresh": "^0.14.0",
"svgo": "^3.3.2",
"ts-node": "^10.9.1",
"ts-plugin-sort-import-suggestions": "^1.0.4",
"typescript": "^5.9.2",
"webpack-bundle-analyzer": "^4.10.1"
},
"resolutions": {
"@react-native/babel-preset": "0.81.4",
"@react-native/normalize-colors": "0.81.4",
"@react-native/babel-preset": "0.81.5",
"@react-native/normalize-colors": "0.81.5",
"**/@expo/image-utils": "0.8.7",
"**/expo-constants": "18.0.8",
"**/expo-device": "7.1.4",
@@ -0,0 +1,389 @@
diff --git a/node_modules/react-native-reanimated/lib/module/component/PerformanceMonitor.js b/node_modules/react-native-reanimated/lib/module/component/PerformanceMonitor.js
index 9c98d6c..70ada9f 100644
--- a/node_modules/react-native-reanimated/lib/module/component/PerformanceMonitor.js
+++ b/node_modules/react-native-reanimated/lib/module/component/PerformanceMonitor.js
@@ -1,125 +1,5 @@
'use strict';
-import React, { useEffect, useRef } from 'react';
-import { StyleSheet, TextInput, View } from 'react-native';
-import { addWhitelistedNativeProps } from "../ConfigHelper.js";
-import { createAnimatedComponent } from "../createAnimatedComponent/index.js";
-import { useAnimatedProps, useFrameCallback, useSharedValue } from "../hook/index.js";
-function createCircularDoublesBuffer(size) {
- 'worklet';
-
- return {
- next: 0,
- buffer: new Float32Array(size),
- size,
- count: 0,
- push(value) {
- const oldValue = this.buffer[this.next];
- const oldCount = this.count;
- this.buffer[this.next] = value;
- this.next = (this.next + 1) % this.size;
- this.count = Math.min(this.size, this.count + 1);
- return oldCount === this.size ? oldValue : null;
- },
- front() {
- const notEmpty = this.count > 0;
- if (notEmpty) {
- const current = this.next - 1;
- const index = current < 0 ? this.size - 1 : current;
- return this.buffer[index];
- }
- return null;
- },
- back() {
- const notEmpty = this.count > 0;
- return notEmpty ? this.buffer[this.next] : null;
- }
- };
-}
-const DEFAULT_BUFFER_SIZE = 20;
-addWhitelistedNativeProps({
- text: true
-});
-const AnimatedTextInput = createAnimatedComponent(TextInput);
-function loopAnimationFrame(fn) {
- let lastTime = 0;
- function loop() {
- requestAnimationFrame(time => {
- if (lastTime > 0) {
- fn(lastTime, time);
- }
- lastTime = time;
- requestAnimationFrame(loop);
- });
- }
- loop();
-}
-function getFps(renderTimeInMs) {
- 'worklet';
-
- return 1000 / renderTimeInMs;
-}
-function completeBufferRoutine(buffer, timestamp) {
- 'worklet';
-
- timestamp = Math.round(timestamp);
- const droppedTimestamp = buffer.push(timestamp) ?? timestamp;
- const measuredRangeDuration = timestamp - droppedTimestamp;
- return getFps(measuredRangeDuration / buffer.count);
-}
-function JsPerformance({
- smoothingFrames
-}) {
- const jsFps = useSharedValue(null);
- const totalRenderTime = useSharedValue(0);
- const circularBuffer = useRef(createCircularDoublesBuffer(smoothingFrames));
- useEffect(() => {
- loopAnimationFrame((_, timestamp) => {
- timestamp = Math.round(timestamp);
- const currentFps = completeBufferRoutine(circularBuffer.current, timestamp);
-
- // JS fps have to be measured every 2nd frame,
- // thus 2x multiplication has to occur here
- jsFps.value = (currentFps * 2).toFixed(0);
- });
- }, [jsFps, totalRenderTime]);
- const animatedProps = useAnimatedProps(() => {
- const text = 'JS: ' + (jsFps.value ?? 'N/A') + ' ';
- return {
- text,
- defaultValue: text
- };
- });
- return <View style={styles.container}>
- <AnimatedTextInput style={styles.text} animatedProps={animatedProps} editable={false} />
- </View>;
-}
-function UiPerformance({
- smoothingFrames
-}) {
- const uiFps = useSharedValue(null);
- const circularBuffer = useSharedValue(null);
- useFrameCallback(({
- timestamp
- }) => {
- if (circularBuffer.value === null) {
- circularBuffer.value = createCircularDoublesBuffer(smoothingFrames);
- }
- timestamp = Math.round(timestamp);
- const currentFps = completeBufferRoutine(circularBuffer.value, timestamp);
- uiFps.value = currentFps.toFixed(0);
- });
- const animatedProps = useAnimatedProps(() => {
- const text = 'UI: ' + (uiFps.value ?? 'N/A') + ' ';
- return {
- text,
- defaultValue: text
- };
- });
- return <View style={styles.container}>
- <AnimatedTextInput style={styles.text} animatedProps={animatedProps} editable={false} />
- </View>;
-}
/**
* A component that lets you measure fps values on JS and UI threads on both the
* Paper and Fabric architectures.
@@ -127,38 +7,7 @@ function UiPerformance({
* @param smoothingFrames - Determines amount of saved frames which will be used
* for fps value smoothing.
*/
-export function PerformanceMonitor({
- smoothingFrames = DEFAULT_BUFFER_SIZE
-}) {
- return <View style={styles.monitor}>
- <JsPerformance smoothingFrames={smoothingFrames} />
- <UiPerformance smoothingFrames={smoothingFrames} />
- </View>;
+export function PerformanceMonitor() {
+ return null;
}
-const styles = StyleSheet.create({
- monitor: {
- flexDirection: 'row',
- position: 'absolute',
- backgroundColor: '#0006',
- zIndex: 1000
- },
- header: {
- fontSize: 14,
- color: '#ffff',
- paddingHorizontal: 5
- },
- text: {
- fontSize: 13,
- fontVariant: ['tabular-nums'],
- color: '#ffff',
- fontFamily: 'monospace',
- paddingHorizontal: 3
- },
- container: {
- alignItems: 'center',
- justifyContent: 'center',
- flexDirection: 'row',
- flexWrap: 'wrap'
- }
-});
//# sourceMappingURL=PerformanceMonitor.js.map
diff --git a/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx b/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx
index ff8fc8a..34dde79 100644
--- a/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx
+++ b/node_modules/react-native-reanimated/src/component/PerformanceMonitor.tsx
@@ -1,170 +1,5 @@
'use strict';
-import React, { useEffect, useRef } from 'react';
-import { StyleSheet, TextInput, View } from 'react-native';
-
-import { addWhitelistedNativeProps } from '../ConfigHelper';
-import { createAnimatedComponent } from '../createAnimatedComponent';
-import type { FrameInfo } from '../frameCallback';
-import { useAnimatedProps, useFrameCallback, useSharedValue } from '../hook';
-
-type CircularBuffer = ReturnType<typeof createCircularDoublesBuffer>;
-function createCircularDoublesBuffer(size: number) {
- 'worklet';
-
- return {
- next: 0 as number,
- buffer: new Float32Array(size),
- size,
- count: 0 as number,
-
- push(value: number): number | null {
- const oldValue = this.buffer[this.next];
- const oldCount = this.count;
- this.buffer[this.next] = value;
-
- this.next = (this.next + 1) % this.size;
- this.count = Math.min(this.size, this.count + 1);
- return oldCount === this.size ? oldValue : null;
- },
-
- front(): number | null {
- const notEmpty = this.count > 0;
- if (notEmpty) {
- const current = this.next - 1;
- const index = current < 0 ? this.size - 1 : current;
- return this.buffer[index];
- }
- return null;
- },
-
- back(): number | null {
- const notEmpty = this.count > 0;
- return notEmpty ? this.buffer[this.next] : null;
- },
- };
-}
-
-const DEFAULT_BUFFER_SIZE = 20;
-addWhitelistedNativeProps({ text: true });
-const AnimatedTextInput = createAnimatedComponent(TextInput);
-
-function loopAnimationFrame(fn: (lastTime: number, time: number) => void) {
- let lastTime = 0;
-
- function loop() {
- requestAnimationFrame((time) => {
- if (lastTime > 0) {
- fn(lastTime, time);
- }
- lastTime = time;
- requestAnimationFrame(loop);
- });
- }
-
- loop();
-}
-
-function getFps(renderTimeInMs: number): number {
- 'worklet';
- return 1000 / renderTimeInMs;
-}
-
-function completeBufferRoutine(
- buffer: CircularBuffer,
- timestamp: number
-): number {
- 'worklet';
- timestamp = Math.round(timestamp);
-
- const droppedTimestamp = buffer.push(timestamp) ?? timestamp;
-
- const measuredRangeDuration = timestamp - droppedTimestamp;
-
- return getFps(measuredRangeDuration / buffer.count);
-}
-
-function JsPerformance({ smoothingFrames }: { smoothingFrames: number }) {
- const jsFps = useSharedValue<string | null>(null);
- const totalRenderTime = useSharedValue(0);
- const circularBuffer = useRef<CircularBuffer>(
- createCircularDoublesBuffer(smoothingFrames)
- );
-
- useEffect(() => {
- loopAnimationFrame((_, timestamp) => {
- timestamp = Math.round(timestamp);
-
- const currentFps = completeBufferRoutine(
- circularBuffer.current,
- timestamp
- );
-
- // JS fps have to be measured every 2nd frame,
- // thus 2x multiplication has to occur here
- jsFps.value = (currentFps * 2).toFixed(0);
- });
- }, [jsFps, totalRenderTime]);
-
- const animatedProps = useAnimatedProps(() => {
- const text = 'JS: ' + (jsFps.value ?? 'N/A') + ' ';
- return { text, defaultValue: text };
- });
-
- return (
- <View style={styles.container}>
- <AnimatedTextInput
- style={styles.text}
- animatedProps={animatedProps}
- editable={false}
- />
- </View>
- );
-}
-
-function UiPerformance({ smoothingFrames }: { smoothingFrames: number }) {
- const uiFps = useSharedValue<string | null>(null);
- const circularBuffer = useSharedValue<CircularBuffer | null>(null);
-
- useFrameCallback(({ timestamp }: FrameInfo) => {
- if (circularBuffer.value === null) {
- circularBuffer.value = createCircularDoublesBuffer(smoothingFrames);
- }
-
- timestamp = Math.round(timestamp);
-
- const currentFps = completeBufferRoutine(circularBuffer.value, timestamp);
-
- uiFps.value = currentFps.toFixed(0);
- });
-
- const animatedProps = useAnimatedProps(() => {
- const text = 'UI: ' + (uiFps.value ?? 'N/A') + ' ';
- return { text, defaultValue: text };
- });
-
- return (
- <View style={styles.container}>
- <AnimatedTextInput
- style={styles.text}
- animatedProps={animatedProps}
- editable={false}
- />
- </View>
- );
-}
-
-export type PerformanceMonitorProps = {
- /**
- * Sets amount of previous frames used for smoothing at highest expectedFps.
- *
- * Automatically scales down at lower frame rates.
- *
- * Affects jumpiness of the FPS measurements value.
- */
- smoothingFrames?: number;
-};
-
/**
* A component that lets you measure fps values on JS and UI threads on both the
* Paper and Fabric architectures.
@@ -172,40 +7,6 @@ export type PerformanceMonitorProps = {
* @param smoothingFrames - Determines amount of saved frames which will be used
* for fps value smoothing.
*/
-export function PerformanceMonitor({
- smoothingFrames = DEFAULT_BUFFER_SIZE,
-}: PerformanceMonitorProps) {
- return (
- <View style={styles.monitor}>
- <JsPerformance smoothingFrames={smoothingFrames} />
- <UiPerformance smoothingFrames={smoothingFrames} />
- </View>
- );
+export function PerformanceMonitor() {
+ return null;
}
-
-const styles = StyleSheet.create({
- monitor: {
- flexDirection: 'row',
- position: 'absolute',
- backgroundColor: '#0006',
- zIndex: 1000,
- },
- header: {
- fontSize: 14,
- color: '#ffff',
- paddingHorizontal: 5,
- },
- text: {
- fontSize: 13,
- fontVariant: ['tabular-nums'],
- color: '#ffff',
- fontFamily: 'monospace',
- paddingHorizontal: 3,
- },
- container: {
- alignItems: 'center',
- justifyContent: 'center',
- flexDirection: 'row',
- flexWrap: 'wrap',
- },
-});
@@ -0,0 +1,33 @@
diff --git a/node_modules/react-native-uitextview/ios/RNUITextViewShadow.swift b/node_modules/react-native-uitextview/ios/RNUITextViewShadow.swift
index c34ba71..13d576a 100644
--- a/node_modules/react-native-uitextview/ios/RNUITextViewShadow.swift
+++ b/node_modules/react-native-uitextview/ios/RNUITextViewShadow.swift
@@ -159,13 +159,23 @@ class RNUITextViewShadow: RCTShadowView {
let maxSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(MAXFLOAT))
let textSize = self.attributedText.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, context: nil)
- var totalLines = self.lineHeight == 0.0 ? 0 : Int(ceil(textSize.height / self.lineHeight))
-
- if self.numberOfLines != 0, totalLines > self.numberOfLines {
- totalLines = self.numberOfLines
+ var finalHeight: CGFloat
+
+ if self.numberOfLines != 0 && self.lineHeight != 0.0 {
+ // numberOfLines is set with custom line height - need to calculate lines and snap to lineHeight multiples
+ // NOTE: this calculation can be inaccurate with fractional font sizes
+ var totalLines = Int(ceil(textSize.height / self.lineHeight))
+ if totalLines > self.numberOfLines {
+ totalLines = self.numberOfLines
+ }
+ finalHeight = CGFloat(totalLines) * self.lineHeight
+ } else {
+ // Either no numberOfLines limit, or no custom lineHeight - use actual text height
+ // (numberOfLines without custom lineHeight is handled by the UITextView's textContainer.maximumNumberOfLines)
+ finalHeight = textSize.height
}
- self.frameSize = CGSize(width: CGFloat(maxWidth), height: CGFloat(CGFloat(totalLines) * self.lineHeight))
+ self.frameSize = CGSize(width: CGFloat(maxWidth), height: finalHeight)
return YGSize(width: Float(self.frameSize.width), height: Float(self.frameSize.height))
}
+5 -1
View File
@@ -7,13 +7,14 @@ import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components
import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX'
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
import {Text as BaseText, type TextProps} from '#/components/Typography'
import {EmojiSad_Stroke2_Corner0_Rounded as EmojiSadIcon} from './icons/Emoji'
export const colors = {
warning: '#FFC404',
}
type Context = {
type: 'info' | 'tip' | 'warning' | 'error'
type: 'info' | 'tip' | 'warning' | 'error' | 'apology'
}
const Context = createContext<Context>({
@@ -29,12 +30,14 @@ export function Icon() {
tip: CircleInfoIcon,
warning: WarningIcon,
error: CircleXIcon,
apology: EmojiSadIcon,
}[type]
const fill = {
info: t.atoms.text_contrast_medium.color,
tip: t.palette.primary_500,
warning: colors.warning,
error: t.palette.negative_500,
apology: t.atoms.text_contrast_medium.color,
}[type]
return <Icon fill={fill} size="md" />
}
@@ -109,6 +112,7 @@ export function Outer({
tip: t.palette.primary_500,
warning: colors.warning,
error: t.palette.negative_500,
apology: t.atoms.border_contrast_high.borderColor,
}[type]
return (
<Context.Provider value={{type}}>
+4 -6
View File
@@ -7,7 +7,6 @@ import {
Pressable,
type PressableProps,
type StyleProp,
StyleSheet,
type TargetedEvent,
type TextProps,
type TextStyle,
@@ -512,8 +511,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
[state, variant, color, size, disabled],
)
const flattenedBaseStyles = flatten([baseStyles, style])
return (
<PressableComponent
role="button"
@@ -533,9 +530,10 @@ export const Button = React.forwardRef<View, ButtonProps>(
a.align_center,
a.justify_center,
a.curve_continuous,
flattenedBaseStyles,
baseStyles,
style,
...(state.hovered || state.pressed
? [hoverStyles, flatten(hoverStyleProp)]
? [hoverStyles, hoverStyleProp]
: []),
]}
onPressIn={onPressIn}
@@ -726,7 +724,7 @@ export function useSharedButtonTextStyles() {
baseStyles.push(a.text_xs, a.leading_snug, a.font_semi_bold)
}
return StyleSheet.flatten(baseStyles)
return flatten(baseStyles)
}, [t, variant, color, size, disabled])
}
+74
View File
@@ -0,0 +1,74 @@
import {TouchableWithoutFeedback, View} from 'react-native'
import * as Clipboard from 'expo-clipboard'
import {atoms as a, useTheme} from '#/alf'
import * as Prompt from '#/components/Prompt'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {useDevMode} from '#/storage/hooks/dev-mode'
/**
* Internal-use component to display debug information supplied by the appview.
* The `debug` field only exists on some API views, and is only visible for
* internal users in dev mode. As such, none of these strings need to be
* translated.
*
* This component can be removed at any time if we don't find it useful.
*/
export function DebugFieldDisplay<T extends {debug?: {[x: string]: unknown}}>({
subject,
}: {
subject: T
}) {
const t = useTheme()
const [devMode] = useDevMode()
const prompt = Prompt.usePromptControl()
if (!devMode) return
if (!subject.debug) return
return (
<>
<Prompt.Basic
control={prompt}
title="Debug"
description={JSON.stringify(subject.debug, null, 2)}
cancelButtonCta="Close"
confirmButtonCta="Copy"
onConfirm={() => {
Clipboard.setStringAsync(JSON.stringify(subject.debug, null, 2))
Toast.show('Copied to clipboard', {type: 'success'})
}}
/>
<TouchableWithoutFeedback
accessibilityRole="button"
onPress={e => {
e.preventDefault()
e.stopPropagation()
prompt.open()
return false
}}>
<View style={[a.flex_row, a.align_center, a.gap_xs, a.pt_sm, a.pb_xs]}>
<View
style={[a.py_xs, a.px_sm, a.rounded_sm, t.atoms.bg_contrast_25]}>
<Text
style={[a.font_bold, a.text_xs, t.atoms.text_contrast_medium]}>
Debug
</Text>
</View>
<Text
numberOfLines={1}
style={[
a.flex_1,
a.text_xs,
a.leading_tight,
{fontFamily: 'monospace'},
t.atoms.text_contrast_low,
]}>
{JSON.stringify(subject.debug)}
</Text>
</View>
</TouchableWithoutFeedback>
</>
)
}
+4 -4
View File
@@ -16,7 +16,7 @@ import {RemoveScrollBar} from 'react-remove-scroll-bar'
import {logger} from '#/logger'
import {useA11y} from '#/state/a11y'
import {useDialogStateControlContext} from '#/state/dialogs'
import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {Context} from '#/components/Dialog/context'
import {
@@ -180,7 +180,7 @@ export function Inner({
onClick={stopPropagation}
onStartShouldSetResponder={_ => true}
onTouchEnd={stopPropagation}
style={flatten([
style={[
a.relative,
a.rounded_md,
a.w_full,
@@ -195,7 +195,7 @@ export function Inner({
},
!reduceMotionEnabled && a.zoom_fade_in,
style,
])}>
]}>
<DismissableLayer.DismissableLayer
onInteractOutside={preventDefault}
onFocusOutside={preventDefault}
@@ -244,7 +244,7 @@ export const InnerFlatList = React.forwardRef<
contentContainerStyle={[a.h_full, a.px_0, webInnerContentContainerStyle]}>
<FlatList
ref={ref}
style={[a.h_full, gtMobile ? a.px_2xl : a.px_xl, flatten(style)]}
style={[a.h_full, gtMobile ? a.px_2xl : a.px_xl, style]}
{...props}
/>
{footer}
+1 -1
View File
@@ -186,7 +186,7 @@ export function Description({
return rt
}, [description])
if (!rt) return null
return <RichText value={rt} style={[a.leading_snug]} disableLinks {...rest} />
return <RichText value={rt} disableLinks {...rest} />
}
export function DescriptionPlaceholder() {
+11 -1
View File
@@ -390,7 +390,12 @@ export function ProfileGrid({
{!isProfileHeaderContext && (
<InlineLinkText
label={_(msg`See more suggested profiles on the Explore page`)}
to="/search">
to="/search"
onPress={() => {
logger.metric('suggestedUser:seeMore', {
logContext: isFeedContext ? 'Explore' : 'Profile',
})
}}>
<Trans>See more</Trans>
</InlineLinkText>
)}
@@ -429,6 +434,11 @@ function SeeMoreSuggestedProfilesCard() {
to="/search"
color="primary"
label={_(msg`Browse more accounts on the Explore page`)}
onPress={() => {
logger.metric('suggestedUser:seeMore', {
logContext: 'Explore',
})
}}
style={[
a.flex_col,
a.align_center,
+2 -11
View File
@@ -2,7 +2,6 @@ import {View} from 'react-native'
import {
atoms as a,
flatten,
type TextStyleProp,
useTheme,
type ViewStyleProp,
@@ -33,17 +32,9 @@ export function IconCircle({
height: size === 'lg' ? 52 : 64,
backgroundColor: t.palette.primary_50,
},
flatten(style),
style,
]}>
<Icon
size={size}
style={[
{
color: t.palette.primary_500,
},
flatten(iconStyle),
]}
/>
<Icon size={size} style={[{color: t.palette.primary_500}, iconStyle]} />
</View>
)
}
+7 -2
View File
@@ -83,7 +83,12 @@ export function InterestTabs({
function handleTabLayout(index: number, x: number, width: number) {
if (!tabOffsets.length) {
pendingTabOffsets.current[index] = {x, width}
if (pendingTabOffsets.current.length === interests.length) {
// not only do we check if the length is equal to the number of interests,
// but we also need to ensure that the array isn't sparse. `.filter()`
// removes any empty slots from the array
if (
pendingTabOffsets.current.filter(o => !!o).length === interests.length
) {
setTabOffsets(pendingTabOffsets.current)
}
}
@@ -205,7 +210,7 @@ export function InterestTabs({
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
snapToOffsets={
tabOffsets.length === interests.length
tabOffsets.filter(o => !!o).length === interests.length
? tabOffsets.map(o => o.x - tokens.space.xl)
: undefined
}
+1 -1
View File
@@ -55,7 +55,7 @@ export function Description({value, handle}: {value?: string; handle: string}) {
const {_} = useLingui()
return value ? (
<Text numberOfLines={2}>
<RichText value={value} style={[a.leading_snug]} />
<RichText value={value} />
</Text>
) : (
<Text emoji style={[a.leading_snug]}>
+2 -2
View File
@@ -166,7 +166,7 @@ export function useLink({
const state = navigation.getState()
// if screen is not in the current navigator, it means it's
// most likely a tab screen. note: state can be undefined
if (!state?.routeNames.includes(screen)) {
if (!state?.routeNames?.includes?.(screen)) {
const parent = navigation.getParent()
if (
parent &&
@@ -296,7 +296,7 @@ export function Link({
return (
<Button
{...rest}
style={[a.justify_start, flatten(rest.style)]}
style={[a.justify_start, rest.style]}
role="link"
accessibilityRole="link"
href={href}
+2 -2
View File
@@ -5,7 +5,7 @@ import {useLingui} from '@lingui/react'
import {cleanError} from '#/lib/strings/errors'
import {CenteredView} from '#/view/com/util/Views'
import {atoms as a, flatten, useBreakpoints, useTheme} from '#/alf'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {Error} from '#/components/Error'
import {Loader} from '#/components/Loader'
@@ -43,7 +43,7 @@ export function ListFooter({
a.pb_lg,
t.atoms.border_contrast_low,
{height: height ?? 180, paddingTop: 30},
flatten(style),
style,
]}>
{isFetchingNextPage ? (
<Loader size="xl" />
+2 -7
View File
@@ -7,7 +7,7 @@ import Animated, {
withTiming,
} from 'react-native-reanimated'
import {atoms as a, flatten, useTheme} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import {type Props, useCommonSVGProps} from '#/components/icons/common'
import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader'
@@ -37,12 +37,7 @@ export function Loader(props: Props) {
]}>
<Icon
{...props}
style={[
a.absolute,
a.inset_0,
t.atoms.text_contrast_high,
flatten(props.style),
]}
style={[a.absolute, a.inset_0, t.atoms.text_contrast_high, props.style]}
/>
</Animated.View>
)
+2 -2
View File
@@ -1,6 +1,6 @@
import {View} from 'react-native'
import {atoms as a, flatten, useTheme} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import {type Props, useCommonSVGProps} from '#/components/icons/common'
import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader'
@@ -24,7 +24,7 @@ export function Loader(props: Props) {
a.absolute,
a.inset_0,
t.atoms.text_contrast_high,
flatten(props.style),
props.style,
]}
/>
</div>
+13 -5
View File
@@ -1,6 +1,8 @@
import {StyleSheet} from 'react-native'
import type React from 'react'
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
import {isHighDPI} from '#/lib/browser'
import {atoms as a, platform, useTheme, type ViewStyleProp} from '#/alf'
import {Fill} from '#/components/Fill'
/**
@@ -25,7 +27,15 @@ export function MediaInsetBorder({
<Fill
style={[
a.rounded_md,
a.border,
{
borderWidth: platform({
native: StyleSheet.hairlineWidth,
// while we generally use hairlineWidth (aka 1px),
// we make an exception here for high DPI screens
// as the 1px border is very noticeable -sfn
web: isHighDPI ? 0.5 : StyleSheet.hairlineWidth,
}),
},
opaque
? [t.atoms.border_contrast_low]
: [
@@ -34,9 +44,7 @@ export function MediaInsetBorder({
: t.atoms.border_contrast_high,
{opacity: 0.6},
],
{
pointerEvents: 'none',
},
a.pointer_events_none,
style,
]}>
{children}
@@ -5,11 +5,11 @@ import {
useSafeAreaInsets,
} from 'react-native-safe-area-context'
import {LinearGradient} from 'expo-linear-gradient'
import {utils} from '@bsky.app/alf'
import {isAndroid, isNative} from '#/platform/detection'
import {useA11y} from '#/state/a11y'
import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf'
import {transparentifyColor} from '#/alf/util/colorGeneration'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {FocusScope} from '#/components/FocusScope'
import {LockScroll} from '#/components/LockScroll'
@@ -47,7 +47,7 @@ export function Overlay({
) : (
<LinearGradient
colors={[
transparentifyColor(t.atoms.bg.backgroundColor, 0),
utils.alpha(t.atoms.bg.backgroundColor, 0),
t.atoms.bg.backgroundColor,
t.atoms.bg.backgroundColor,
]}
@@ -97,7 +97,7 @@ export function Overlay({
]}>
<LinearGradient
colors={[
transparentifyColor(t.atoms.bg.backgroundColor, 0),
utils.alpha(t.atoms.bg.backgroundColor, 0),
t.atoms.bg.backgroundColor,
]}
start={[0.5, 0]}
@@ -113,7 +113,7 @@ export function Overlay({
role="dialog"
aria-role="dialog"
aria-label={label}
style={flatten([
style={[
a.relative,
a.w_full,
a.p_2xl,
@@ -128,7 +128,7 @@ export function Overlay({
maxWidth: 420,
}),
],
])}>
]}>
{children}
</View>
</FocusScope>
@@ -5,3 +5,10 @@ import {ID} from '#/components/PolicyUpdateOverlay/updates/202508/config'
* the relationship is clear.
*/
export const ACTIVE_UPDATE_ID = ID
/**
* Toggle to enable or disable the policy update overlay feature e.g. once an
* update has run its course, set this to false. For new updates, set this to
* true and change `ACTIVE_UPDATE_ID` to the new update ID.
*/
export const POLICY_UPDATE_IS_ENABLED = false
+10 -2
View File
@@ -7,6 +7,7 @@ import {
} from 'react'
import {useSession} from '#/state/session'
import {POLICY_UPDATE_IS_ENABLED} from '#/components/PolicyUpdateOverlay/config'
import {Provider as PortalProvider} from '#/components/PolicyUpdateOverlay/Portal'
import {
type PolicyUpdateState,
@@ -45,8 +46,15 @@ export function Provider({children}: {children?: ReactNode}) {
const {hasSession} = useSession()
const [isReadyToShowOverlay, setIsReadyToShowOverlay] = useState(false)
const state = usePolicyUpdateState({
// only enable the policy update overlay in non-test environments
enabled: isReadyToShowOverlay && hasSession && ENV !== 'e2e',
enabled:
// if the feature is enabled
POLICY_UPDATE_IS_ENABLED &&
// once shell has rendered
isReadyToShowOverlay &&
// only once logged in
hasSession &&
// only enabled in non-test environments
ENV !== 'e2e',
})
const ctx = useMemo(
+4 -1
View File
@@ -64,7 +64,10 @@ export function ImageEmbed({
}
const onPressIn = (_: number) => {
InteractionManager.runAfterInteractions(() => {
Image.prefetch(items.map(i => i.uri))
Image.prefetch(
items.map(i => i.uri),
'memory',
)
})
}
+3 -1
View File
@@ -57,7 +57,9 @@ export function PostRepliedTo({
size="xs"
style={[t.atoms.text_contrast_medium, {top: -1}]}
/>
<Text style={textStyle}>{label}</Text>
<Text style={[a.flex_1, textStyle]} numberOfLines={1}>
{label}
</Text>
</View>
)
}
@@ -258,6 +258,7 @@ let PostMenuItems = ({
const onHidePost = () => {
hidePost({uri: postUri})
logEvent('thread:click:hideReplyForMe', {})
}
const hideInPWI = !!postAuthor.labels?.find(
@@ -338,6 +339,12 @@ let PostMenuItems = ({
replyUri: postUri,
action,
})
// Log metric only when hiding (not when showing)
if (isHide) {
logEvent('thread:click:hideReplyForEveryone', {})
}
Toast.show(
isHide
? _(msg`Reply was successfully hidden`)
@@ -50,7 +50,6 @@ export function RecentChats({postUri}: {postUri: string}) {
style={[a.flex_1, a.pt_2xs, {minHeight: 98}]}
contentContainerStyle={[a.gap_sm, a.px_md]}
showsHorizontalScrollIndicator={false}
fadingEdgeLength={64}
nestedScrollEnabled>
{convos && convos.length > 0 ? (
convos.map(convo => {
+82 -7
View File
@@ -1,4 +1,4 @@
import {memo, useState} from 'react'
import {memo, useMemo, useState} from 'react'
import {type StyleProp, View, type ViewStyle} from 'react-native'
import {
type AppBskyFeedDefs,
@@ -25,9 +25,10 @@ import {
useProgressGuideControls,
} from '#/state/shell/progress-guide'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, flatten, useBreakpoints} from '#/alf'
import {atoms as a, useBreakpoints} from '#/alf'
import {Reply as Bubble} from '#/components/icons/Reply'
import {useFormatPostStatCount} from '#/components/PostControls/util'
import * as Skele from '#/components/Skeleton'
import {BookmarkButton} from './BookmarkButton'
import {
PostControlButton,
@@ -188,11 +189,11 @@ let PostControls = ({
})
}
const secondaryControlSpacingStyles = flatten([
{gap: 0}, // default, we want `gap` to be defined on the resulting object
variant !== 'compact' && a.gap_xs,
(big || gtPhone) && a.gap_sm,
])
const secondaryControlSpacingStyles = useSecondaryControlSpacingStyles({
variant,
big,
gtPhone,
})
return (
<View
@@ -336,3 +337,77 @@ let PostControls = ({
}
PostControls = memo(PostControls)
export {PostControls}
export function PostControlsSkeleton({
big,
style,
variant,
}: {
big?: boolean
style?: StyleProp<ViewStyle>
variant?: 'compact' | 'normal' | 'large'
}) {
const {gtPhone} = useBreakpoints()
const rowHeight = big ? 32 : 28
const padding = 4
const size = rowHeight - padding * 2
const secondaryControlSpacingStyles = useSecondaryControlSpacingStyles({
variant,
big,
gtPhone,
})
const itemStyles = {
padding,
}
return (
<Skele.Row
style={[a.flex_row, a.justify_between, a.align_center, a.gap_md, style]}>
<View style={[a.flex_row, a.flex_1, {maxWidth: 320}]}>
<View
style={[itemStyles, a.flex_1, a.align_start, {marginLeft: -padding}]}>
<Skele.Pill blend size={size} />
</View>
<View style={[itemStyles, a.flex_1, a.align_start]}>
<Skele.Pill blend size={size} />
</View>
<View style={[itemStyles, a.flex_1, a.align_start]}>
<Skele.Pill blend size={size} />
</View>
</View>
<View style={[a.flex_row, a.justify_end, secondaryControlSpacingStyles]}>
<View style={itemStyles}>
<Skele.Circle blend size={size} />
</View>
<View style={itemStyles}>
<Skele.Circle blend size={size} />
</View>
<View style={itemStyles}>
<Skele.Circle blend size={size} />
</View>
</View>
</Skele.Row>
)
}
function useSecondaryControlSpacingStyles({
variant,
big,
gtPhone,
}: {
variant?: 'compact' | 'normal' | 'large'
big?: boolean
gtPhone: boolean
}) {
return useMemo(() => {
let gap = 0 // default, we want `gap` to be defined on the resulting object
if (variant !== 'compact') gap = a.gap_xs.gap
if (big || gtPhone) gap = a.gap_sm.gap
return {gap}
}, [variant, big, gtPhone])
}
+1 -1
View File
@@ -396,7 +396,7 @@ export function Description({
<View style={[a.pt_xs]}>
<RichText
value={rt}
style={[a.leading_snug, style]}
style={style}
numberOfLines={numberOfLines}
disableLinks
/>
@@ -515,7 +515,12 @@ function Inner({
<View style={[a.flex_row, a.align_center, a.pt_md, a.pb_xs]}>
<Text
numberOfLines={1}
style={[a.text_lg, a.font_semi_bold, a.self_start]}>
style={[
a.text_lg,
a.leading_snug,
a.font_semi_bold,
a.self_start,
]}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
@@ -4,6 +4,7 @@ import {type ModerationOpts} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
import {logEvent} from '#/lib/statsig/statsig'
import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -13,10 +14,6 @@ import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggeste
import {useSession} from '#/state/session'
import {type Follow10ProgressGuide} from '#/state/shell/progress-guide'
import {type ListMethods} from '#/view/com/util/List'
import {
popularInterests,
useInterestsDisplayNames,
} from '#/screens/Onboarding/state'
import {
atoms as a,
native,
+3 -7
View File
@@ -48,18 +48,14 @@ export function RichText({
[value],
)
const flattenedStyle = flatten(style)
const plainStyles = [a.leading_snug, flattenedStyle]
const interactiveStyles = [
a.leading_snug,
flatten(interactiveStyle),
flattenedStyle,
]
const plainStyles = [a.leading_snug, style]
const interactiveStyles = [plainStyles, interactiveStyle]
const {text, facets} = richText
if (!facets?.length) {
if (isOnlyEmoji(text)) {
const flattenedStyle = flatten(style)
const fontSize =
(flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
return (
+2 -1
View File
@@ -96,7 +96,8 @@ export function RichTextTag({
onLongPress={createStaticClick(menuProps.onPress).onPress}
accessibilityHint={hint}
label={label}
style={textStyle}>
style={textStyle}
emoji>
{isNative ? (
display
) : (
-9
View File
@@ -39,15 +39,6 @@ export function SubtleHover({
/>
)
if (hover) {
console.log({
isWeb,
web,
isNative,
native,
})
}
if (isWeb && web) {
return isTouchDevice ? null : el
} else if (isNative && native) {
+4 -4
View File
@@ -1,9 +1,9 @@
import {Children, createContext, useContext, useMemo} from 'react'
import {View} from 'react-native'
import {utils} from '@bsky.app/alf'
import {Popover} from 'radix-ui'
import {atoms as a, flatten, select, useTheme} from '#/alf'
import {transparentifyColor} from '#/alf/util/colorGeneration'
import {
ARROW_SIZE,
BUBBLE_MAX_WIDTH,
@@ -80,9 +80,9 @@ export function Content({
{
minWidth: 'max-content',
boxShadow: select(t.name, {
light: `0 0 24px ${transparentifyColor(t.palette.black, 0.2)}`,
dark: `0 0 24px ${transparentifyColor(t.palette.black, 0.2)}`,
dim: `0 0 24px ${transparentifyColor(t.palette.black, 0.2)}`,
light: `0 0 24px ${utils.alpha(t.palette.black, 0.2)}`,
dark: `0 0 24px ${utils.alpha(t.palette.black, 0.2)}`,
dim: `0 0 24px ${utils.alpha(t.palette.black, 0.2)}`,
}),
},
])}>
+3 -3
View File
@@ -1,7 +1,7 @@
import {UITextView} from 'react-native-uitextview'
import {logger} from '#/logger'
import {atoms, flatten, useAlf, useTheme, web} from '#/alf'
import {atoms, useAlf, useTheme, web} from '#/alf'
import {
childHasEmoji,
normalizeTextStyles,
@@ -26,7 +26,7 @@ export function Text({
}: TextProps) {
const {fonts, flags} = useAlf()
const t = useTheme()
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)], {
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, style], {
fontScale: fonts.scaleMultiplier,
fontFamily: fonts.family,
flags,
@@ -84,7 +84,7 @@ export function P({style, ...rest}: TextProps) {
<Text
{...attr}
{...rest}
style={[atoms.text_md, atoms.leading_normal, flatten(style)]}
style={[atoms.text_md, atoms.leading_relaxed, style]}
/>
)
}
+3 -3
View File
@@ -8,7 +8,7 @@ import {FocusGuards, FocusScope} from 'radix-ui/internal'
import {logger} from '#/logger'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {Logo} from '#/view/icons/Logo'
import {atoms as a, flatten, useBreakpoints, web} from '#/alf'
import {atoms as a, useBreakpoints, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {Text} from '#/components/Typography'
@@ -78,7 +78,7 @@ export function WelcomeModal({control}: WelcomeModalProps) {
]}>
<FocusScope.FocusScope asChild loop trapped>
<View
style={flatten([
style={[
{
maxWidth: 800,
maxHeight: 600,
@@ -89,7 +89,7 @@ export function WelcomeModal({control}: WelcomeModalProps) {
a.rounded_lg,
a.overflow_hidden,
a.zoom_in,
])}>
]}>
<ImageBackground
source={welcomeModalBg}
style={[a.flex_1, a.justify_center]}
+1 -1
View File
@@ -300,7 +300,7 @@ export function GifPreview({
a.flex_1,
a.mb_sm,
a.rounded_sm,
a.aspect_card,
a.aspect_square,
{opacity: pressed ? 0.8 : 1},
t.atoms.bg_contrast_25,
]}
@@ -13,6 +13,7 @@ import isEqual from 'lodash.isequal'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {useMyListsQuery} from '#/state/queries/my-lists'
import {useGetPost} from '#/state/queries/post'
import {
createPostgateQueryKey,
getPostgateRecord,
@@ -25,12 +26,15 @@ import {
} from '#/state/queries/postgate/util'
import {
createThreadgateViewQueryKey,
getThreadgateView,
type ThreadgateAllowUISetting,
threadgateViewToAllowUISetting,
useSetThreadgateAllowMutation,
useThreadgateViewQuery,
} from '#/state/queries/threadgate'
import {
PostThreadContextProvider,
usePostThreadContext,
} from '#/state/queries/usePostThread'
import {useAgent, useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
@@ -133,10 +137,13 @@ export type PostInteractionSettingsDialogProps = {
export function PostInteractionSettingsDialog(
props: PostInteractionSettingsDialogProps,
) {
const postThreadContext = usePostThreadContext()
return (
<Dialog.Outer control={props.control}>
<Dialog.Handle />
<PostInteractionSettingsDialogControlledInner {...props} />
<PostThreadContextProvider context={postThreadContext}>
<PostInteractionSettingsDialogControlledInner {...props} />
</PostThreadContextProvider>
</Dialog.Outer>
)
}
@@ -558,6 +565,7 @@ export function usePrefetchPostInteractionSettings({
}) {
const queryClient = useQueryClient()
const agent = useAgent()
const getPost = useGetPost()
return React.useCallback(async () => {
try {
@@ -570,7 +578,10 @@ export function usePrefetchPostInteractionSettings({
}),
queryClient.prefetchQuery({
queryKey: createThreadgateViewQueryKey(rootPostUri),
queryFn: () => getThreadgateView({agent, postUri: rootPostUri}),
queryFn: async () => {
const post = await getPost({uri: rootPostUri})
return post.threadgate ?? null
},
staleTime: STALE.SECONDS.THIRTY,
}),
])
@@ -579,5 +590,5 @@ export function usePrefetchPostInteractionSettings({
safeMessage: e.message,
})
}
}, [queryClient, agent, postUri, rootPostUri])
}, [queryClient, agent, postUri, rootPostUri, getPost])
}
@@ -435,7 +435,7 @@ function DialogInner({
multiline
label={_(msg`Description`)}
placeholder={descriptionPlaceholder}
testID="editProfileDescriptionInput"
testID="editListDescriptionInput"
/>
</TextField.Root>
{descriptionTooLong && (
@@ -134,12 +134,12 @@ function MenuInner({
accessibilityRole="button"
role="button"
onPress={() => setExpanded(true)}
style={flatten([
style={[
a.rounded_full,
{height: 34, width: 34},
a.justify_center,
a.align_center,
])}>
]}>
<DotGridIcon size="lg" style={t.atoms.text_contrast_medium} />
</Pressable>
</DropdownMenu.Item>
+1 -1
View File
@@ -424,7 +424,7 @@ function PreviewMessage({message}: {message: ChatBskyConvoDefs.MessageView}) {
]}>
<RichText
value={rt}
style={[a.text_md, a.leading_snug]}
style={[a.text_md]}
interactiveStyle={a.underline}
enableTags
/>
+2 -3
View File
@@ -6,7 +6,6 @@ import {HITSLOP_10} from '#/lib/constants'
import {isNative} from '#/platform/detection'
import {
atoms as a,
flatten,
native,
type TextStyleProp,
useTheme,
@@ -233,7 +232,7 @@ export function Item({
onPressOut={onPressOut}
onFocus={onFocus}
onBlur={onBlur}
style={[a.flex_row, a.align_center, a.gap_sm, flatten(style)]}>
style={[a.flex_row, a.align_center, a.gap_sm, style]}>
{typeof children === 'function' ? children(state) : children}
</Pressable>
</ItemContext.Provider>
@@ -260,7 +259,7 @@ export function LabelText({
native({
paddingTop: 2,
}),
flatten(style),
style,
]}>
{children}
</Text>
+1 -1
View File
@@ -40,7 +40,7 @@ export function createSinglePathSVG({path}: {path: string}) {
viewBox="0 0 24 24"
width={size}
height={size}
style={[style]}>
style={style}>
{gradient}
<Path fill={fill} fillRule="evenodd" clipRule="evenodd" d={path} />
</Svg>
+1 -4
View File
@@ -56,10 +56,7 @@ function DialogInner({profile}: {profile: bsky.profile.AnyProfileView}) {
const date = new Date()
date.setMinutes(date.getMinutes() + offset)
return i18n
.date(date, {hour: 'numeric', minute: '2-digit', hour12: true})
.toLocaleUpperCase()
.replace(' ', '')
return i18n.date(date, {hour: 'numeric', minute: '2-digit', hour12: true})
},
[tick, i18n],
)
+6 -1
View File
@@ -85,7 +85,11 @@ function ContentHiderActive({
blur.type !== 'label' ||
(blur.type === 'label' && blur.source.type !== 'user')
) {
return desc.name
if (desc.isSubjectAccount) {
return _(msg`${desc.name} (Account)`)
} else {
return desc.name
}
}
let hasAdultContentLabel = false
@@ -127,6 +131,7 @@ function ContentHiderActive({
modui?.blurs,
blur,
desc.name,
desc.isSubjectAccount,
labelDefs,
i18n.locale,
globalLabelStrings,
+18 -2
View File
@@ -1,6 +1,7 @@
import React from 'react'
import React, {useState} from 'react'
import {View} from 'react-native'
import {type ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api'
import {XRPCError} from '@atproto/xrpc'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useMutation} from '@tanstack/react-query'
@@ -19,6 +20,7 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {Admonition} from '../Admonition'
import {Divider} from '../Divider'
import {Loader} from '../Loader'
@@ -228,6 +230,7 @@ function AppealForm({
const sourceName = labeler
? sanitizeHandle(labeler.creator.handle, '@')
: label.src
const [error, setError] = useState<string | null>(null)
const {mutate, isPending} = useMutation({
mutationFn: async () => {
@@ -252,8 +255,16 @@ function AppealForm({
)
},
onError: err => {
if (err instanceof XRPCError && err.error === 'AlreadyAppealed') {
setError(
_(
msg`You've already appealed this label and it's being reviewed by our moderation team.`,
),
)
} else {
setError(_(msg`Failed to submit appeal, please try again.`))
}
logger.error('Failed to submit label appeal', {message: err})
Toast.show(_(msg`Failed to submit appeal, please try again.`), 'xmark')
},
onSuccess: () => {
control.close()
@@ -285,6 +296,11 @@ function AppealForm({
</Trans>
</Text>
</View>
{error && (
<Admonition type="error" style={[a.mt_sm]}>
{error}
</Admonition>
)}
<View style={[a.my_md]}>
<Dialog.Input
label={_(msg`Text input field`)}
+1
View File
@@ -2,3 +2,4 @@ export const isSafari = false
export const isFirefox = false
export const isTouchDevice = true
export const isAndroidWeb = false
export const isHighDPI = true
+1
View File
@@ -6,3 +6,4 @@ export const isFirefox = /firefox|fxios/i.test(navigator.userAgent)
export const isTouchDevice = window.matchMedia('(pointer: coarse)').matches
export const isAndroidWeb =
/android/i.test(navigator.userAgent) && isTouchDevice
export const isHighDPI = window.matchMedia('(min-resolution: 2dppx)').matches
+1
View File
@@ -78,6 +78,7 @@ const NETWORK_ERRORS = [
'Network request failed',
'Failed to fetch',
'Load failed',
'Upstream service unreachable',
]
export function isNetworkError(e: unknown) {
+8 -2
View File
@@ -1,10 +1,11 @@
import React from 'react'
import {Alert} from 'react-native'
import * as Linking from 'expo-linking'
import * as WebBrowser from 'expo-web-browser'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {isIOS, isNative} from '#/platform/detection'
import {useSession} from '#/state/session'
import {useCloseAllActiveElements} from '#/state/util'
import {
@@ -32,7 +33,12 @@ export function useIntentHandler() {
const {tryApplyUpdate} = useApplyPullRequestOTAUpdate()
React.useEffect(() => {
const handleIncomingURL = (url: string) => {
const handleIncomingURL = async (url: string) => {
if (isIOS) {
// Close in-app browser if it's open (iOS only)
await WebBrowser.dismissBrowser().catch(() => {})
}
const referrerInfo = Referrer.getReferrerInfo()
if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
logger.metric('deepLink:referrerReceived', {
+8 -1
View File
@@ -12,7 +12,7 @@ import {
import {isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {isIOS} from '#/platform/detection'
import {isAndroid, isIOS} from '#/platform/detection'
import {IS_TESTFLIGHT} from '#/env'
const MINIMUM_MINIMIZE_TIME = 15 * 60e3
@@ -193,6 +193,13 @@ export function useOTAUpdates() {
return
}
// TEMP: disable wake-from-background OTA loading on Android.
// This is causing a crash when the thread view is open due to
// `maintainVisibleContentPosition`. See repro repo for more details:
// https://github.com/mozzius/ota-crash-repro
// Old Arch only - re-enable once we're on the New Archictecture! -sfn
if (isAndroid) return
const subscription = AppState.addEventListener(
'change',
async nextAppState => {
+78
View File
@@ -0,0 +1,78 @@
import {useMemo} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export const interests = [
'animals',
'art',
'books',
'comedy',
'comics',
'culture',
'dev',
'education',
'finance',
'food',
'gaming',
'journalism',
'movies',
'music',
'nature',
'news',
'pets',
'photography',
'politics',
'science',
'sports',
'tech',
'tv',
'writers',
] as const
export type Interest = (typeof interests)[number]
// most popular selected interests
export const popularInterests = [
'art',
'gaming',
'sports',
'comics',
'music',
'politics',
'photography',
'science',
'news',
] satisfies Interest[]
export function useInterestsDisplayNames() {
const {_} = useLingui()
return useMemo<Record<string, string>>(() => {
return {
// Keep this alphabetized
animals: _(msg`Animals`),
art: _(msg`Art`),
books: _(msg`Books`),
comedy: _(msg`Comedy`),
comics: _(msg`Comics`),
culture: _(msg`Culture`),
dev: _(msg`Software Dev`),
education: _(msg`Education`),
finance: _(msg`Finance`),
food: _(msg`Food`),
gaming: _(msg`Video Games`),
journalism: _(msg`Journalism`),
movies: _(msg`Movies`),
music: _(msg`Music`),
nature: _(msg`Nature`),
news: _(msg`News`),
pets: _(msg`Pets`),
photography: _(msg`Photography`),
politics: _(msg`Politics`),
science: _(msg`Science`),
sports: _(msg`Sports`),
tech: _(msg`Tech`),
tv: _(msg`TV`),
writers: _(msg`Writers`),
} satisfies Record<Interest, string>
}, [_])
}
+45 -6
View File
@@ -114,8 +114,39 @@ export async function saveImageToMediaLibrary({uri}: {uri: string}) {
// as the starting image, or put it directly into the album
const album = await MediaLibrary.getAlbumAsync(ALBUM_NAME)
if (album) {
// if album exists, put the image straight in there
await MediaLibrary.createAssetAsync(imagePath, album)
// try and migrate if needed
try {
if (await MediaLibrary.albumNeedsMigrationAsync(album)) {
await MediaLibrary.migrateAlbumIfNeededAsync(album)
}
} catch (err) {
logger.info('Attempted and failed to migrate album', {
safeMessage: err,
})
}
try {
// if album exists, put the image straight in there
await MediaLibrary.createAssetAsync(imagePath, album)
} catch (err) {
logger.info('Failed to create asset', {safeMessage: err})
// however, it's possible that we don't have write permission to the album
// try making a new one!
try {
await MediaLibrary.createAlbumAsync(
ALBUM_NAME,
undefined,
undefined,
imagePath,
)
} catch (err2) {
logger.info('Failed to create asset in a fresh album', {
safeMessage: err2,
})
// ... and if all else fails, just put it in DCIM
await MediaLibrary.createAssetAsync(imagePath)
}
}
} else {
// otherwise, create album with asset (albums must always have at least one asset)
await MediaLibrary.createAlbumAsync(
@@ -126,12 +157,13 @@ export async function saveImageToMediaLibrary({uri}: {uri: string}) {
)
}
} else {
await MediaLibrary.createAssetAsync(imagePath)
await MediaLibrary.saveToLibraryAsync(imagePath)
}
} catch (err) {
logger.error(err instanceof Error ? err : String(err), {
message: 'Failed to save image to media library',
})
throw err
} finally {
safeDeleteAsync(imagePath)
}
@@ -378,14 +410,21 @@ function createPath(ext: string) {
async function downloadImage(uri: string, path: string, timeout: number) {
const dlResumable = createDownloadResumable(uri, path, {cache: true})
const to1 = setTimeout(() => dlResumable.cancelAsync(), timeout)
let timedOut = false
const to1 = setTimeout(() => {
timedOut = true
dlResumable.cancelAsync()
}, timeout)
const dlRes = await dlResumable.downloadAsync()
clearTimeout(to1)
if (!dlRes?.uri) {
throw new Error('Failed to download image - dlRes is undefined')
if (timedOut) {
throw new Error('Failed to download image - timed out')
} else {
throw new Error('Failed to download image - dlRes is undefined')
}
}
return normalizePath(dlRes.uri)
+32
View File
@@ -0,0 +1,32 @@
import {useCallback} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isNative} from '#/platform/detection'
import * as Toast from '#/components/Toast'
import {saveImageToMediaLibrary} from './manip'
/**
* Same as `saveImageToMediaLibrary`, but also handles permissions and toasts
*
* iOS doesn't not require permissions to save images to the media library,
* so this file is platform-split as it's much simpler than the Android version.
*/
export function useSaveImageToMediaLibrary() {
const {_} = useLingui()
return useCallback(
async (uri: string) => {
if (!isNative) {
throw new Error('useSaveImageToMediaLibrary is native only')
}
try {
await saveImageToMediaLibrary({uri})
Toast.show(_(msg`Image saved`))
} catch (e: any) {
Toast.show(_(msg`Failed to save image: ${String(e)}`), {type: 'error'})
}
},
[_],
)
}
+18 -9
View File
@@ -1,15 +1,17 @@
import {useCallback} from 'react'
import * as MediaLibrary from 'expo-media-library'
import {t} from '@lingui/macro'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isNative} from '#/platform/detection'
import * as Toast from '#/view/com/util/Toast'
import * as Toast from '#/components/Toast'
import {saveImageToMediaLibrary} from './manip'
/**
* Same as `saveImageToMediaLibrary`, but also handles permissions and toasts
*/
export function useSaveImageToMediaLibrary() {
const {_} = useLingui()
const [permissionResponse, requestPermission, getPermission] =
MediaLibrary.usePermissions({
granularPermissions: ['photo'],
@@ -23,9 +25,12 @@ export function useSaveImageToMediaLibrary() {
async function save() {
try {
await saveImageToMediaLibrary({uri})
Toast.show(t`Image saved`)
Toast.show(_(msg`Image saved`))
} catch (e: any) {
Toast.show(t`Failed to save image: ${String(e)}`, 'xmark')
Toast.show(_(msg`Failed to save image: ${String(e)}`), {
type: 'error',
})
}
}
@@ -42,18 +47,22 @@ export function useSaveImageToMediaLibrary() {
} else {
// since we've been explicitly denied, show a toast.
Toast.show(
t`Images cannot be saved unless permission is granted to access your photo library.`,
'xmark',
_(
msg`Images cannot be saved unless permission is granted to access your photo library.`,
),
{type: 'error'},
)
}
} else {
Toast.show(
t`Permission to access your photo library was denied. Please enable it in your system settings.`,
'xmark',
_(
msg`Permission to access your photo library was denied. Please enable it in your system settings.`,
),
{type: 'error'},
)
}
}
},
[permissionResponse, requestPermission, getPermission],
[permissionResponse, requestPermission, getPermission, _],
)
}
@@ -1,3 +1,5 @@
import {type ModerationUI} from '@atproto/api'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import type * as bsky from '#/types/bsky'
@@ -5,9 +7,10 @@ import type * as bsky from '#/types/bsky'
export function createSanitizedDisplayName(
profile: bsky.profile.AnyProfileView,
noAt = false,
moderation?: ModerationUI,
) {
if (profile.displayName != null && profile.displayName !== '') {
return sanitizeDisplayName(profile.displayName)
return sanitizeDisplayName(profile.displayName, moderation)
} else {
return sanitizeHandle(profile.handle, noAt ? '' : '@')
}
@@ -28,6 +28,7 @@ export interface ModerationCauseDescription {
sourceType?: ModerationCauseSource['type']
sourceAvi?: string
sourceDid?: string
isSubjectAccount?: boolean
}
export function useModerationCauseDescription(
@@ -162,6 +163,7 @@ export function useModerationCauseDescription(
sourceType: cause.source.type,
sourceAvi: labeler?.creator.avatar,
sourceDid: cause.label.src,
isSubjectAccount: cause.label.uri.startsWith('did:'),
}
}
// should never happen
+4 -1
View File
@@ -7,6 +7,7 @@ import debounce from 'lodash.debounce'
import {PUBLIC_APPVIEW_DID, PUBLIC_STAGING_APPVIEW_DID} from '#/lib/constants'
import {logger as notyLogger} from '#/lib/notifications/util'
import {isNetworkError} from '#/lib/strings/errors'
import {isNative} from '#/platform/detection'
import {useAgeAssuranceContext} from '#/state/ageAssurance'
import {type SessionAccount, useAgent, useSession} from '#/state/session'
@@ -47,7 +48,9 @@ async function _registerPushToken({
notyLogger.debug(`registerPushToken: success`)
} catch (error) {
notyLogger.error(`registerPushToken: failed`, {safeMessage: error})
if (!isNetworkError(error)) {
notyLogger.error(`registerPushToken: failed`, {safeMessage: error})
}
}
}

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