Compare commits

...

308 Commits

Author SHA1 Message Date
Eric Bailey 53f65fac32 Add some logs 2024-04-03 13:54:03 -05:00
Dan Abramov 2e2e554f07 Scope query client per DID 2024-03-22 00:06:25 +00:00
Dan Abramov 8e8bcfbc75 Remove useQueryClient dependency from session provider 2024-03-21 23:41:44 +00:00
Dan Abramov 9e36c4abb0 Pull useQueryClient-dependent code down in App.native 2024-03-21 23:38:55 +00:00
Dan Abramov 17b014f4ae Move QueryProvider inside the key 2024-03-21 23:30:33 +00:00
Dan Abramov 57a72a1108 Factor out QueryProvider 2024-03-21 23:24:17 +00:00
Dan Abramov 19031bc699 Make notifications init reactive to queryClient 2024-03-21 23:14:51 +00:00
Dan Abramov f18b801979 Pass queryClient explicitly to updateProfileShadow 2024-03-21 22:53:00 +00:00
Dan Abramov e56b7e00b5 Pass queryClient explicitly to updatePostShadow 2024-03-21 22:47:02 +00:00
Dan Abramov 698ba036dd Pass queryClient explicitly to resetProfilePostsQueries 2024-03-21 22:41:24 +00:00
Eric Bailey 4d50aac4ee Release fixes (#3318)
* Fix error overflow

* await setSavedFeeds before setting birth date

* await birth date first instead of feeds

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2024-03-21 12:11:23 -07:00
Samuel Newman e874dd290e Merge pull request #3313 from bluesky-social/samuel/package-manager-field
Specify package manager in package.json
2024-03-21 17:25:40 +00:00
Eric Bailey 5f39ca3187 Mods UI fixes (#3296)
* Fix report dialog buttons on Android by adjusting styles

* Dry up label pref comp
2024-03-21 12:21:36 -05:00
Hailey 4ff2bb7aba Fix missing error on PostThread (#3295) 2024-03-21 10:16:04 -07:00
Eric Bailey eb0ee6ae1c Fix dialogs state references, fixes Android back button (#3314)
* Fix active/open dialogs state

* Improve close logic
2024-03-21 12:06:39 -05:00
dan 55fb81867b [Statsig] Protect against early logEvent call crashing (#3315)
* [Statsig] Check if initialized

* Never interrupt the caller
2024-03-21 17:01:55 +00:00
Eric Bailey 7503d83eaa Fix android splash jump (#3316)
* Setup translucent status bar of splash screen on android (#3300)

Co-authored-by: Eric Bailey <git@esb.lol>

* Fix conflict better

---------

Co-authored-by: Eiichi Yoshikawa <edo@bari-ikutsu.com>
2024-03-21 11:53:01 -05:00
Samuel Newman a8ccc91335 Specify package manager in package.json 2024-03-21 16:00:36 +00:00
dan 396d183dfc [Statsig] Onboarding and routing events (#3302) 2024-03-21 11:04:02 +00:00
Hailey ad3dd9f6dc Fix problems with BottomSheet and the report dialog (#3297)
* use @discord/bottom-sheet

* add @types/invariant

* some progress on keyboard dialog

* rework

rework

add a comment

use discord bottom sheet

* remove `@gorhom/bottom-sheet`

* remove android specific code

* organize imports
2024-03-20 17:26:38 -07:00
Samuel Newman c649ee1afa Merge pull request #3217 from bluesky-social/samuel/alf-login
Use ALF for login & signup flow
2024-03-20 23:29:24 +00:00
Samuel Newman d2d4d3a092 bump pkg json version 2024-03-20 23:28:53 +00:00
Eric Bailey 6bf97d4fd5 Adjust DateField timezone offset 2024-03-20 17:44:31 -05:00
Eric Bailey 0dbc796ff6 Fix key error on choose account 2024-03-20 17:41:17 -05:00
Eric Bailey 19fab671a3 Move some things around 2024-03-20 17:25:08 -05:00
Samuel Newman 8ad813cd86 Merge pull request #3294 from bluesky-social/samuel/alf-splash
Use ALF for Splashscreen
2024-03-20 22:23:35 +00:00
Eric Bailey 58588efcea Bump button size on choose account 2024-03-20 17:18:07 -05:00
Eric Bailey b0fcfa563d Password flow tweaks 2024-03-20 17:14:13 -05:00
Eric Bailey 70f5cc6a9f Login form tweaks 2024-03-20 17:00:35 -05:00
Eric Bailey 5c08f0d95b Remove log 2024-03-20 16:46:37 -05:00
Eric Bailey bcffaffc07 StepInfo tweaks 2024-03-20 16:42:07 -05:00
Eric Bailey aa239451af Signup shell tweaks 2024-03-20 16:26:30 -05:00
Eric Bailey 6e1541f203 HostingProvider tweaks 2024-03-20 16:15:41 -05:00
Samuel Newman 94cc08929f fix pencil svg in asset folder 2024-03-20 20:18:31 +00:00
Samuel Newman 1d10946f54 sort imports for files related to this PR 2024-03-20 19:44:52 +00:00
Samuel Newman 823ce63796 Merge remote-tracking branch 'origin/main' into samuel/alf-login 2024-03-20 19:36:22 +00:00
Samuel Newman 81bc3ed4c3 Merge pull request #3189 from bluesky-social/samuel/sort-imports
Sort imports using eslint-plugin-simple-import-sort
2024-03-20 19:35:53 +00:00
Samuel Newman fb17afc99d dismiss keyboard when opening dialog 2024-03-20 18:02:18 +00:00
Samuel Newman cf9d884b5b use inlinelink instead of textlink 2024-03-20 17:22:36 +00:00
Samuel Newman eacd1cbe2a use alf for splashscreen (web) 2024-03-20 17:11:59 +00:00
Samuel Newman adcd097136 use alf for splashscreen (native) 2024-03-20 17:11:41 +00:00
Samuel Newman 708d0fe3e1 skip initial entering animation for login 2024-03-20 16:10:07 +00:00
Samuel Newman a0839723ee login form back button goes to chooseaccount conditionally 2024-03-20 15:58:54 +00:00
Samuel Newman 6d1307fdc7 rm extra atom 2024-03-20 15:54:55 +00:00
Samuel Newman 4c60d4d070 remove unnecessary group component 2024-03-20 15:42:39 +00:00
Samuel Newman 09e9769eea add logEvent that got lost in merge from main 2024-03-20 15:40:03 +00:00
Samuel Newman d24ffba01d Merge remote-tracking branch 'origin/main' into samuel/alf-login 2024-03-20 15:37:14 +00:00
Paul Frazee 023e12bb4e Run intl:extract (again) 2024-03-19 20:30:03 -07:00
Paul Frazee 832d5734b4 Merge branch 'main' of github.com:bluesky-social/social-app into main 2024-03-19 20:29:13 -07:00
dan 20337ceef1 [Statsig] Track active time (#3289) 2024-03-19 20:25:37 -07:00
Paul Frazee b5bda17812 Merge branch 'main' of github.com:bluesky-social/social-app into main 2024-03-19 20:24:18 -07:00
dan 3d8d1dd173 [Statsig] Track login/logout (#3286)
* [Statsig] Track login/logout

* Fix missing attribution
2024-03-19 20:24:05 -07:00
Paul Frazee 6255e33b50 Run intl:extract 2024-03-19 20:22:52 -07:00
Paul Frazee 2e2fae378a Set keyboardDismisMode to interactive on the report dialog (#3288) 2024-03-19 20:21:25 -07:00
dan b6c9d34e45 [Statsig] Track feed refresh (#3283) 2024-03-19 17:56:31 -07:00
Paul Frazee 54f424d047 Various e2e test fixes (#3284)
* Just use the first picture every time

* Add missing testIDs

* Various test fixes

* Use simplified link fetcher for e2e

* Disable tests for now-n

* Update test-env creation
2024-03-19 17:54:40 -07:00
Hailey a90566d864 use different labels for porn and sexual (#3282)
* use different labels for `porn` and `sexual`

* newline
2024-03-19 17:47:10 -07:00
Samuel Newman 2fa26ceedc hog FormError 2024-03-19 22:16:29 +00:00
Samuel Newman 49cd7e4bcf add pressed state and rm cloneElement 2024-03-19 22:06:59 +00:00
Samuel Newman 1f02ed5d87 change choose account form to use Button + add hover styles 2024-03-19 22:03:49 +00:00
Samuel Newman 2428d22368 change hosting provider and forgot pw touchables to button 2024-03-19 21:58:53 +00:00
Paul Frazee bdf77f8548 Fix detection of !no-unauthenticated (#3279) 2024-03-19 14:43:00 -07:00
Samuel Newman 8556016a69 remove textfield references from hosting provider 2024-03-19 21:37:12 +00:00
cdfzo 43be4f7d3d Fix typo in tagMenuSearchByUser (#3181) 2024-03-19 14:32:59 -07:00
Samuel Newman 717c53bea8 remove explicit height/width 2024-03-19 21:30:41 +00:00
Samuel Newman fd448a5fab rename pencil -> pencilLine and reexport 2024-03-19 21:28:18 +00:00
Samuel Newman 793710b02b rm package manager field 2024-03-19 21:14:51 +00:00
Samuel Newman 8b5279ce15 animate login spinner 2024-03-19 21:10:31 +00:00
Eric Bailey 89709949ab Bump sdk for mute words updates (#3277) 2024-03-19 14:08:07 -07:00
Paul Frazee f2fc654f34 Merge branch 'alexkuz-web-fix-fonts-and-image-box' into main 2024-03-19 14:07:36 -07:00
Paul Frazee e433a1c968 Remove dead style 2024-03-19 14:06:04 -07:00
Paul Frazee a2ed2c3b22 Merge branch 'web-fix-fonts-and-image-box' of https://github.com/alexkuz/social-app into alexkuz-web-fix-fonts-and-image-box 2024-03-19 14:05:43 -07:00
Samuel Newman b226f41560 use in-button spinner instead of activity indicator 2024-03-19 20:56:13 +00:00
Hailey ccf53af2ba Prevent linking to post from an embed in composer (#3275) 2024-03-19 13:26:25 -07:00
dan ebf8644df9 Track notification open (#3274) 2024-03-19 13:19:11 -07:00
Dovgonosyk 0cac671e8a Update Ukrainian translation (#2951)
Co-authored-by: Ukrainians <github-actions@github.com>
2024-03-19 13:13:27 -07:00
surfdude29 69f8ea80f8 Fix typos and clarify two accessibilityHints (#2923)
* Clarify accessibilityHint in Settings

* Fix typo in ModerationMutedAccounts.tsx

* Fix typo in ModerationBlockedAccounts.tsx

* fix lint error in ModerationMutedAccounts.tsx

* try again

* fix lint error in index.tsx

* try again

* Update index.tsx

* Update index.tsx

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2024-03-19 13:11:48 -07:00
dan 5bec587717 [Statsig] Include OS and track app state changes (#3273)
* Include platform in identify

* Track back/foregrounding
2024-03-19 13:11:26 -07:00
Paul Frazee e9222a9d97 Merge branch 'jaoler-main' into main 2024-03-19 13:04:52 -07:00
Paul Frazee 40908e9ce7 Fix formatting 2024-03-19 13:04:45 -07:00
Paul Frazee 5dc41eb0d9 Merge branch 'main' of https://github.com/jaoler/Bluesky-app-Finnish-translations into jaoler-main 2024-03-19 13:04:06 -07:00
Paul Frazee d966b3e425 Fix merge error 2024-03-19 12:56:29 -07:00
Samuel Newman 10943b4ff4 fix merge issue (double hook) 2024-03-19 19:52:27 +00:00
Samuel Newman 4794ab6b9a Merge remote-tracking branch 'origin/main' into samuel/alf-login 2024-03-19 19:51:35 +00:00
Hailey a1c4f19731 Use ALF for signup flow, improve a11y of signup (#3151)
* Use ALF for signup flow, improve a11y of signup

* adjust padding

* rm log

* org imports

* clarify allowance of hyphens

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* fix a few accessibility items

* Standardise date input across platforms (#3223)

* make the date input consistent across platforms

* integrate into new signup form

* rm log

* add transitions

* show correct # of steps

* use `FormError`

* animate buttons

* use `ScreenTransition`

* fix android text overflow via flex -> flex_1

* change button color

* (android) make date input the same height as others

* fix deps

* fix deps

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2024-03-19 12:47:46 -07:00
Ivan Beà 5621c80425 Update catalan localization messages.po (#2904)
* Update catalan localization messages.po

Localization of new lines

* Update messages.po

* Update messages.po

Corrections applied
2024-03-19 12:44:22 -07:00
Leonid 0e2619f7d5 Update zh-CN translation to latest version (#2901)
* Update Chinese translation for "followed you" message

* Update translations for zh-CN locale

* Update messages.po

* Delete empty line

* Add translations in messages.po

* fix some issue

* Apply suggestions from code review

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

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

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

* accept suggestions

* unify translations

* Update Chinese translations for Navigation and PreferencesHomeFeed

* Update translations

* merge latest translations

---------

Co-authored-by: Frudrax Cheng <i@cynosura.one>
2024-03-19 12:43:41 -07:00
Paul Frazee d14be05922 Merge branch 'Titianbeetle-main' into main 2024-03-19 12:42:43 -07:00
Paul Frazee 0ae9da83df Merge branch 'main' of https://github.com/Titianbeetle/social-app into Titianbeetle-main 2024-03-19 12:42:37 -07:00
Gildásio Filho f685f9c8c0 Update pt-BR localization to latest version (#2876)
* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

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

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

* Update messages.po

---------

Co-authored-by: Gleydson Rodrigues <gleydsonsr@gmail.com>
2024-03-19 12:31:33 -07:00
Paul Frazee 9e4f81b156 Merge branch 'quiple-patch-3' into main 2024-03-19 12:29:57 -07:00
Paul Frazee 14982b9393 Merge branch 'patch-3' of https://github.com/quiple/social-app into quiple-patch-3 2024-03-19 12:29:22 -07:00
Minseo Lee dac73cbeea Update Korean localization (#2769)
* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

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

Co-authored-by: Lens0021 / Leslie <lorentz0021@gmail.com>

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

---------

Co-authored-by: Lens0021 / Leslie <lorentz0021@gmail.com>
Co-authored-by: Daniel S. Park <soohan@heartade.dev>
2024-03-19 12:24:23 -07:00
Eric Bailey dfe88e1656 Use consistent avatar shape/defaults for labelers (#3257)
* Add type: labeler to easy spots

* Search and ProfileCard

* Filter out of suggested follows

* ComposeReplyTo

* PReviewable avatar in posts

* Lists

* PostMeta

* Notifications

* Autocomplete

* Straggler

* Bump sdk
2024-03-19 12:18:57 -07:00
Hailey b9474a5d55 ProfileFollows and ProfileFollowers cleanup (#3219)
* cleanup PostThread

rm some more unnecessary code

cleanup some more pieces

fix `isLoading` logic

few fixes

organize

refactor `PostThread`

allow chaining of `postThreadQuery`

Update `Hashtag` screen with the component changes

Make some changes to the List components

adjust height and padding of bottom loader to account for bottom bar

* rm unnecessary chaining logic

* maxReplies logic

* adjust error logic

* use `<` instead of `<=`

* add back warning comment

* remove unused prop

* adjust order

* implement list improvements for followers/follows

* update prop name

* small adjustments

fix flex

add window size

adjust isLoading

* remove log

* don't show retry for no results

* don't show error if `isLoading`
2024-03-19 12:11:20 -07:00
Hailey addd66b37f PostThread cleanup (#3183)
* cleanup PostThread

rm some more unnecessary code

cleanup some more pieces

fix `isLoading` logic

few fixes

organize

refactor `PostThread`

allow chaining of `postThreadQuery`

Update `Hashtag` screen with the component changes

Make some changes to the List components

adjust height and padding of bottom loader to account for bottom bar

* rm unnecessary chaining logic

* maxReplies logic

* adjust error logic

* use `<` instead of `<=`

* add back warning comment

* remove unused prop

* adjust order

* update prop name

* don't show error if `isLoading`
2024-03-19 12:10:10 -07:00
Hailey 07e6d001a2 patch-fix RCTRefreshControl (#3259) 2024-03-19 12:05:28 -07:00
Hailey 2255d21bda prevent duplicate pickers from opening on android (#3240)
* prevent duplicate pickers from opening on android

* revert unnecessary changes

* test

* one more test
2024-03-19 12:03:16 -07:00
Hailey 5e0a6a12ff Update trusted hosts, allow #, and add more tests (#3232)
* Update trusted hosts, allow `#`, and add more tests

* update comments
2024-03-19 12:01:54 -07:00
Eric Bailey 5b4b8e47d9 Use default label prefs for pwi (#3271) 2024-03-19 11:39:56 -07:00
Paul Frazee c9c3bd98b7 Rework the labeler selection step of the report flow (#3269)
* Rework the labeler selection step of the report flow

* Fix: use gtMobile

* Use primitives, fix avatar

* Spacing tweaks

* Show handle instead of description

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2024-03-19 11:38:17 -07:00
Hailey 9277282e6c add expo-updates github action (#3270)
* add expo-updates github action

* add two scripts
2024-03-19 11:18:22 -07:00
surfdude29 a02d5f9eb9 Update French translation (#3117)
* Update French translation

* Apply suggestions from code review

Co-authored-by: Stanislas Signoud <signez@stanisoft.net>

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

Co-authored-by: Stanislas Signoud <signez@stanisoft.net>

* Change `étiquette` to `mot-clé`

* Change other usages of `actualité` to `actu`

* Update messages.po

* Change certain space characters to non-breaking spaces

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

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

Co-authored-by: Stanislas Signoud <signez@stanisoft.net>

* Update messages.po

* Remove superseded strings and match some

* Add new translations

* Update revision date

---------

Co-authored-by: Stanislas Signoud <signez@stanisoft.net>
2024-03-19 11:08:33 -07:00
surfdude29 1cefe70ebe Update German translations (#2917)
* Update German translations

* Apply suggestions from code review

Co-authored-by: Felix Siebeneicker <felix.siebeneicker@outlook.de>

* Further suggestions from code review

* Apply further suggested change

* Apply suggestions from code review

* Apply suggested changes from PythooonUser

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

* Apply suggestions from code review

Co-authored-by: cdfzo <cdfzo@pm.me>

* Apply suggestions from @cdfzo's code review manually

* Apply suggestion from @cdfzo's code review manually

* Update messages.po

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

Co-authored-by: Felix Siebeneicker <felix.siebeneicker@outlook.de>

* Update messages.po

* Apply suggestions from code review

Co-authored-by: Felix Siebeneicker <felix.siebeneicker@outlook.de>

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

* Update messages.po

* Change `Benachrichtigungen` to `Mitteilungen` in line with #2805

* Apply suggestions from code review

Co-authored-by: Felix Siebeneicker <felix.siebeneicker@outlook.de>

* Apply suggestions from code review

Co-authored-by: Felix Siebeneicker <felix.siebeneicker@outlook.de>

* Change four usages of Benutzerhandle

* Update messages.po

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

* Add credits

* Update messages.po

* Add new translated strings following merge

* Update messages.po

* Update messages.po

* Update german translations by @cdfzo

* Update german translations

* Apply suggestions from code review

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Correct translations from pr review

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Update revision date of messages.po

* Update messages.po

* Update messages.po

* Delete superseded strings

* Add new translations

* Update messages.po

* Apply suggestions from code review

Co-authored-by: cdfzo <cdfzo@pm.me>

* Apply suggested change from code review by @cdfzo

* Update revision date of messages.po

---------

Co-authored-by: Felix Siebeneicker <felix.siebeneicker@outlook.de>
Co-authored-by: cdfzo <cdfzo@pm.me>
2024-03-19 11:07:26 -07:00
Eric Bailey b622f63918 Mobile mod label setting component (#3267)
* Mobile mod label setting component

* Bump label title size

* Dont show disabled label config on mobile

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2024-03-19 09:56:11 -07:00
Samuel Newman bd8d8fb9f1 change error to warn 2024-03-19 16:13:15 +00:00
Eric Bailey 877eab0fe3 Fix space on bottom of mod screen (#3266)
* Fix space on bottom of mod screen

* fix PWI label overflow + center loader (#3268)

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2024-03-19 11:04:41 -05:00
Samuel Newman b6903419a1 simplify gotoForm 2024-03-19 15:31:00 +00:00
Samuel Newman 49a392bfb3 rm useless function wrap 2024-03-19 15:29:25 +00:00
Samuel Newman 08d12d9a3d move FormError to components/forms 2024-03-19 15:28:06 +00:00
Samuel Newman f491bd89cc Merge remote-tracking branch 'origin/main' into samuel/alf-login 2024-03-19 15:18:29 +00:00
Hailey 2a5b0ab2ac add Partial<GestureResponderEvent> to .open() (#3227) 2024-03-19 08:14:29 -07:00
Eric Bailey 8ac5144a58 Bday modal tweaks (#3252)
* Smol tweaks to bday dialog

* Juse use existing DateInput for now

* Remove unused code

* Remove passed-in prefs

* Adjust load state

* Revert "Adjust load state"

This reverts commit 802459fd04.

* Fix type error
2024-03-19 09:57:14 -05:00
Hailey 4de8e8fa14 fix borders for labels tab (#3261)
* fix borders for labels tab

* rm entire wrapping view
2024-03-19 09:56:28 -05:00
Samuel Newman 9c49b209ca Merge pull request #3253 from bluesky-social/samuel/square-border-radii
Fix nested border radius issue on square avatars
2024-03-19 14:51:08 +00:00
Minseo Lee ad43d594c9 Merge remote-tracking branch 'upstream/main' into patch-3 2024-03-19 10:52:29 +09:00
Paul Frazee 3abf302b0b Bump @atproto/api@0.12.0 (#3260)
* Bump @atproto/api@0.12.0

* Fix unit tests
2024-03-18 17:46:25 -07:00
Paul Frazee 959121f394 Update the reporting flow to first select a recipient if the user has multiple labelers (#3258) 2024-03-18 16:15:57 -07:00
Paul Frazee 1b10c7bc08 Use entryway for labeler getServices (#3256) 2024-03-18 14:58:02 -07:00
Samuel Newman 248f51a977 change default avi to use a rect instead of a path 2024-03-18 20:08:58 +00:00
Samuel Newman 63fee70dfe change outer border radius to 10 2024-03-18 20:08:46 +00:00
Paul Frazee 20d463ff2f 3p moderation services [WIP] (#2550)
* Add modservice screen and profile-header-card

* Drop the guidelines for now

* Remove ununsed constants

* Add label & label group descriptions

* Not found state

* Reorg, add icon

* Subheader

* Header

* Complete header

* Clean up

* Add all groups

* Fix scroll view

* Dialogs side quest

* Remove log

* Add (WIP) debug mod page

* Dialog solution

* Add note

* Clean up and reorganize localized moderation strings

* Memoize

* Add example

* Add first ReportDialog screen

* Report dialog step 2

* Submit

* Integrate updates

* Move moderation screen

* Migrate buttons

* Migrate everything

* Rough sketch

* Fix types

* Update atoms values

* Abstract ModerationServiceCard

* Hook up data to settings page

* Handle subscription

* Rough enablement

* Rough enablement

* Some validation, fixes

* More work on the mod debug screen

* Hook up data

* Update invalidation

* Hook up data to ReportDialog

* Fix native error

* Refactor/rewrite the entire moderation-application system

* Fix toggles

* Add copyright and other option to report

* Handle reports on profile vs content

* Little cleanup

* Get post hiding back in gear

* Better loading flow on Mod screen

* Clean up Mod screen

* Clean up ProfileMod screen

* Handle muting correctly

* Update enablement on ProfileMod screen

* Improve Moderation screen and dialog

* Styling, handle disabled labelers

* Rework list of labels on own content

* Use moderateNotification()

* ReportDialog updates

* Fix button overflow

* Simplify the ProfileModerationService ui

* Mod screen design

* Move moderation card from the profile header to a tab

* Small tweaks to the moderation screen

* Enable toggle on mod page

* Add notifs to debugmod and dont filter notifs from followed users

* Add moderator-service profile view

* Wire up more of the modservice data to profiles

* A bunch of speculative non-working UI

* Cleanup: delete old code

* Update ModerationDetailsDialog

* Update ReportDialog

* Update LabelsOnMe dialog

* Handle ReportDialog load better

* Rename LabelsOnMeDialog, fix close

* Experiment to put labeling under a tab of a normal profile

* Moderator variation of profile

* Remove dead code and start moving toward latest modsdk

* Remove a bunch of now-dead label strings

* Update ModDebug to be a bit more intuitive and support custom labels

* Minor ui tweaks

* Improve consistency of display name blurring

* Fix profile-card warning rendering

* More debugmod UI tuning

* Update to use new labeler semantics

* Delete some dead code and do some refactoring

* Update profile to pull from labeler definition

* Implement new label config controls (wip)

* Tweak ui

* Implement preference controls on labelers

* Rework label pref ui

* Get moderation screen working

* Add asyncstorage query persistence

* Implement label handling

* Small cleanup

* Implement Likes dialog

* Fix: remove text outside of text element

* Cleanup

* Fix likes dialog on mobile

* Implement the label appeal flow

* Get report flow working again with temporarily fixed report options

* Update onboarding

* Enforce limit of ten labeler subscriptions

* Fix type errors

* Fix lint errors

* Improve types of RQ

* Some work on Likes dialog, needs discussion

* Bit of ReportDialog cleanup

* Replace non-single-path SVG

* Update nudity descriptions

* Update to use new sdk updates

* Add adult-content-enabled behavior to label config

* Use the default setting of custom labels

* Handle global moderation label prefs with the global settings

* Fix missing postAuthor

* Fix empty moderation page

* Add mutewords control back to Mod screen

* Tweak adult setting styles

* Remove deprecated global labels

* Handle underage users on mod screen

* Adjust font sizes

* Swap in RichText

* Like button improvements

* Tweaks to Labeler profile

* Design tweaks for mod pref dialog

* Add tertiary button color

* Switch moderation UIs to tertiary color

* Update mutewords and hiddenposts to use the new sdk

* Add test-environment mod authority

* Switch 'gore' to 'graphic-media'

* Move nudity out of the adult content control

* Remove focus styles from buttons - let the browser behavior handle it

* Fixes to the adult content age-gating in moderaiton

* Ditch tertiary button color, lighten secondary button

* Fix some colors

* Remove focused overrides from toggles

* Liked by screen

* Rework the moderationlabelpref

* Fix optimistic like

* Cleanup

* Change how onboarding handles adult content enabled/disabled

* Add special handling of the mod authorities

* Tweaks

* Update the default labeler avatar to a shield

* Add route to go server

* Avoid dups due to bad config

* Fix attrs

* Fix: dont try to detect link/label mismatches on post meta

* Correctly show the label behavior when adult content is disabled

* Readd the local hiddenPosts handling

* WIP

* Fix bad merge

* Conten hider design tweaks

* Fix text string breakage

* Adjust source text in ContentHider

* Fix link bug

* Design tweaks to ContentHider and ModDetailsDialog

* Adjust spacing of inform badges

* Adjust spacing of embeds in posts

* Style tweaks to post/profile alerts

* Labels on me and dialog

* Remove bad focus styles from post dropdown

* Better spacing solution

* Tune moderation UIs

* Moderation UI tweaks for mobile

* Move labelers query on Mod screen

* Update to use new SDK appLabelers semantics

* Implement report submission

* Replace the report modal entirely with the report dialog

* Add @ to mod details dialog handle

* Bump SDK package

* Remove silly type

* Add to AWS build CI

* Fix ToggleButton overflow

* Clean up ModServiceCard, rename to LabelingServiceCard

* Hackfix to translate gore labels to graphic-media

* Tune content hider sizing on web desktop

* Handle self labels

* Fix spacing below text-only posts

* Fix: send appeals to the right labeler

* Give mod page links interactive states

* Fix references

* Remove focus handling

* Remove remnant

* Remove the like count from the subscribed labeler listing

* Bump @atproto/api@0.11.1

* Remove extra @

* Fix: persist labels to local storage to reduce coverage gaps

* update dipendencies

* revert dipendencies

* Add some explainers on how blocking affects labelers

* Tweak copy

* Fix underline color in header

* Fix profile menu

* Handle card overflow

* Remove metrics from header

* Mute 'account' not 'user'

* Show metrics if self

* Show the labels tab on logged out view

* Fix bad merge

* Use purple theming on labelers

* Tighten space on LabelerCard

* Set staleTime to 6hrs for labeler details

* Memoize the memoizers

* Drop staleTime to 60s

* Move label defs into a context to reduce recomputes

* Submit view tweaks

* Move labeler fetch below auth

* Mitigation: hardcode the bluesky moderation labeler name

* Bump sdk

* Add missing translated string

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Add missing translated string

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Hailey's fix for incorrect profile tabs

Co-authored-by: Hailey <me@haileyok.com>

* Feedback

* Fix borders, add bottom space

* Hailey's fix pt 2

Co-authored-by: Hailey <me@haileyok.com>

* Fix post tabs

* Integrate feedback pt 1

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Integrate feedback pt 2

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Integrate feedback pt 3

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Integrate feedback pt 4

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Integrate feedback pt 5

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Integrate feedback pt 6

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Integrate feedback pt 7

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Integrate feedback pt 8

Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>

* Format

* Integrate new bday modal

* Use public agent for getServices

* Update casing

---------

Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>
Co-authored-by: Hailey <me@haileyok.com>
2024-03-18 12:46:28 -07:00
Hailey d5ebbeb3fc Use [bot] in action username (#3250) 2024-03-18 09:59:39 -07:00
Samuel Newman 1dcd5c116f Merge pull request #3247 from bluesky-social/samuel/dynamic-translations
Fix instances of static translations (`t` macro)
2024-03-18 16:14:00 +00:00
Hailey 88ab83bd52 Properly fill container for YT Shorts videos (#3238)
account for jest

account for jest

yt iframe fill container
2024-03-18 09:09:08 -07:00
Samuel Newman 574c8b913d Add missing macro
Co-authored-by: Gildásio Filho <gildasiogx@gmail.com>
2024-03-18 16:01:48 +00:00
Hailey 79175e2a09 Fix PR labeler bot comment delete (#3249) 2024-03-18 08:40:43 -07:00
Samuel Newman fbfb78b860 change usage of t to msg 2024-03-18 14:37:30 +00:00
Alexander K 54dd4af312 remove scrollbar gutter for Chrome when body scroll is locked 2024-03-17 22:20:50 +01:00
Alexander K 0e3b2633b9 Add 'Liberation Sans' font for FireFox on Linux; lightbox buttons adjustments 2024-03-17 21:34:52 +01:00
Minseo Lee 73dae9f7b5 Update Modal.tsx 2024-03-16 17:17:54 +09:00
Minseo Lee 86d036cca8 Update Modal.tsx 2024-03-16 17:04:45 +09:00
Minseo Lee a3786ee5cd Update Modal.tsx 2024-03-16 17:02:05 +09:00
Minseo Lee 12d8f0071a Update ReasonOptions.tsx 2024-03-16 16:56:56 +09:00
Minseo Lee b4480738e3 Update ReasonOptions.tsx 2024-03-16 16:50:15 +09:00
Minseo Lee 7a0bf7266a Update Lists.tsx 2024-03-16 16:48:58 +09:00
Minseo Lee 6d57094c6a Merge branch 'bluesky-social:main' into patch-3 2024-03-16 16:23:04 +09:00
Samuel Newman d2a11f3344 translate error 2024-03-15 22:08:06 +00:00
Hailey 98bca69ae9 Adjust PR Labeler (#3224) 2024-03-15 13:42:51 -07:00
Hailey fa8b21cea7 Add package diff PR labeler (#3212)
* add PR labeler

* test cache

* rm change
2024-03-15 12:28:21 -07:00
Samuel Newman e30b3d9b3c Merge pull request #3218 from bluesky-social/samuel/alf-birthday
Use ALF for the birthday modal and remove legacy one
2024-03-15 16:45:17 +00:00
Hailey 39da1cd465 add padding to the ScrollableInner 2024-03-15 09:30:19 -07:00
Samuel Newman 38ea041136 change to scrollable inner 2024-03-15 16:22:09 +00:00
Samuel Newman 58ac22c45b make alignment platform specific rather than size specific 2024-03-15 16:21:15 +00:00
Samuel Newman 502df521c1 center date input 2024-03-15 16:03:08 +00:00
Samuel Newman 884f37c349 rename BirthdaySettings -> BirthDateSettings + remove legacy modal 2024-03-15 15:50:54 +00:00
Samuel Newman 577091d44d ALF the birthday modal and remove legacy one 2024-03-15 15:45:58 +00:00
Samuel Newman 273fe3e8e9 scrollview persist taps, autoFocus code 2024-03-15 14:21:38 +00:00
Samuel Newman 0f67be3627 animate transitions on web only 2024-03-15 14:21:15 +00:00
Samuel Newman e31fcb1a6d reenable set new password button 2024-03-15 13:54:23 +00:00
Samuel Newman 7d69570a44 run ticket through svgomg 2024-03-15 13:53:57 +00:00
Samuel Newman a1fc95f30e convert password reset flow 2024-03-15 13:49:13 +00:00
Samuel Newman f71ec52517 Merge remote-tracking branch 'origin/main' into samuel/alf-login 2024-03-15 12:11:34 +00:00
Minseo Lee 242b899f79 Merge branch 'bluesky-social:main' into patch-3 2024-03-15 15:38:31 +09:00
Hailey 4f8381678d Remove FixedTouchableHighlight , fix Android press issues (#3214)
* rm `FixedTouchableHighlight`

* adjust delay for highlight

* remove unnecessary background colors to support background ripple
2024-03-14 18:46:06 -07:00
Minseo Lee d9b94f54ef Merge branch 'bluesky-social:main' into patch-3 2024-03-15 08:59:56 +09:00
Samuel Newman 6279c5cf31 Merge pull request #3209 from bluesky-social/samuel/loggedout-warning-2
Add warning about sharing if post author has the !no-unauthenticated label
2024-03-14 18:52:47 +00:00
Samuel Newman 8316f97e27 fix collapsed input issue 2024-03-14 18:25:39 +00:00
Samuel Newman 78a836d2b1 add warning to profile as well 2024-03-14 15:30:40 +00:00
Samuel Newman 7bdaa624b7 use prompt.basic instead of full prompt 2024-03-14 15:30:27 +00:00
Minseo Lee 4813f26158 Merge branch 'bluesky-social:main' into patch-3 2024-03-14 21:11:00 +09:00
dan 1c25c76645 [Statsig] Track posting, end reached (#3206)
* Track post create

* Track feed endReached
2024-03-14 00:21:42 +00:00
Samuel Newman a06f6ada4e experiment: animate between screens 2024-03-13 23:47:01 +00:00
Samuel Newman 9f5289a101 alf the login form 2024-03-13 23:34:01 +00:00
dan 7eaa573b57 [Statsig] Track likes, reposts, follows (#3195)
* [Statsig] Track likes

* Move tracking to intent

* Track repost/unrepost

* Track profile follows/unfollows

* Less copy paste

* Reorder
2024-03-13 22:31:51 +00:00
Samuel Newman f5b39f2755 convert base login component and ChooseAccountForm 2024-03-13 22:16:32 +00:00
Hailey db79c918b2 1.73 Version Bump (#3200) 2024-03-13 12:06:03 -07:00
Samuel Newman 44b3a37f65 Merge pull request #3201 from bluesky-social/samuel/handle-invalid-files
Filter out non-image files from image picker
2024-03-13 17:59:03 +00:00
Samuel Newman 8c7f813d87 filter out files with non-image mime types 2024-03-13 17:38:37 +00:00
Samuel Newman 41b10e3df5 forgot to save comment whoops 2024-03-13 15:51:55 +00:00
Samuel Newman 4d2efd84e4 sort relative imports and move unprefixed ones 2024-03-13 15:49:24 +00:00
Samuel Newman 70c76a76a0 add custom import order 2024-03-13 14:50:25 +00:00
dan f1d55f49fa Send route name with Statsig events (#3194)
* Add types to Statsig events

* Send route name with events
2024-03-13 03:29:03 +00:00
Samuel Newman 125ef1c756 Revert "run linter"
This reverts commit 1512b5cf68.
2024-03-13 01:55:14 +00:00
Minseo Lee 1760043f79 Update index.tsx 2024-03-13 10:41:38 +09:00
Minseo Lee 2938a1397e Update index.tsx 2024-03-13 10:41:34 +09:00
Minseo Lee 3ead08ab26 Merge branch 'bluesky-social:main' into patch-3 2024-03-13 10:30:07 +09:00
Eric Bailey 653240bc05 Tweak prompt styles on mobile web (#3193) 2024-03-12 20:12:45 -05:00
dan 7af338ed21 Unfork Statsig for web (#3192) 2024-03-13 00:33:47 +00:00
Eric Bailey 202adb6d7b Fix dropdown close via a portaled backdrop (#3191) 2024-03-12 19:25:58 -05:00
Eric Bailey 5c771050bc Fix sensitivity while scrolling (#3190) 2024-03-12 19:10:44 -05:00
Hailey 9f2f7f221c ALF confirmation dialogs (Dialogs Pt. 3) (#3143)
* Improve a11y on ios

* Format

* Remove android

* Fix android

* ALF confirmation dialog

* Use ALF for Delete Post confirmation

organize

diff

fix text

minimize

change copy

alternative confirm prompt

revert type changes

add ButtonColor param

* small adjustment to buttons in prompt

* full width below gtmobile

* update hide post dialog

* space out dialogs

* update dialogs for lists

* add example

* add to app passwords

* Revert some changes

* use sharedvalue for `importantForAccessibility`

* add back `isOpen`

* fix some more types

* small adjustment to buttons in prompt

* full width below gtmobile

* update the rest of the prompts

rm old confirm modal

rm update prompt

feed error prompt

feed source card and profile block/unblock

composer discard

* Update src/view/screens/AppPasswords.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* lint

* How about a default

* Reverse reverse

* Port over confirm dialogs

* Add some comments

* Remove unused file

* complete merge

* add testID where needed

---------

Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2024-03-12 16:56:14 -07:00
Samuel Newman 1512b5cf68 run linter 2024-03-12 22:02:28 +00:00
Samuel Newman a3c09f9f86 add and configure import sort plugin 2024-03-12 21:57:56 +00:00
Hailey 090b35e52e Use new menu for Profile (#3168)
* use new menu on profile

* organize imports

* fix testID

* add person icons

* use `style` prop for minWidth

* use new icons

* rm circleban

* Add unfollow option if account is blocked/blocking

* use `StyleProp` 🤯

* ts after merge

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2024-03-12 14:06:12 -07:00
Alice 70ad820d64 npx update-browserslist-db@latest (#3166)
Should silence the annoying warning for a bit.
2024-03-12 20:39:24 +00:00
Eric Bailey eeba0387b7 Small fixes (#3184)
* Fix alignment of MutedWords close button

* Add cancel button to dropdowns

* Revert "Add cancel button to dropdowns"

This reverts commit b8f5ddce92.
2024-03-12 14:33:02 -05:00
Eric Bailey c9d821c572 Combine actions, convert to new menu (#3174)
* Combine actions, convert to new menu

* remove about tab and move content to header

* Tweak alignment

* fix missing rkey

* hog the like button

* Add a little more whitespace

* Improve a11y

* Yeah toast

* Update usage

* Pin to Home

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2024-03-12 13:50:53 -05:00
Hailey 8123299192 Convert profile edit avatar/banner dropdown menus to new menu (#3177)
* convert profile edit dropdown menu to new menu

fix banner text

add `showCancel` prop to menu outer

banner dropdown to menu

add Cancel button to menu

replace user avatar dropdown with menu

add StreamingLive icon

add camera icon

* remove export

* use new camera icon

* adjust icon color
2024-03-12 10:17:27 -07:00
Hailey 80cc1f18a2 Save image to user media library when taken from camera during composing (#3180)
* save images to media library when taken from camera

* ensure we have access to media library

* `canAskAgain`

* just use MediaLibrary directly to avoid duplication
2024-03-12 09:46:43 -07:00
Hailey ee57d74765 Dedupe navigation events (push, navigate, pop, etc) (#3179) 2024-03-12 09:46:25 -07:00
Eric Bailey b8afb935f4 Unwrap Menu.Trigger on web (#3182) 2024-03-12 11:23:01 -05:00
Samuel Newman 17d921fd9d Merge pull request #3178 from bluesky-social/scrolltotop-overlap
Move scroll to top button inline if viewport is narrow (vertically)
2024-03-12 15:22:40 +00:00
Minseo Lee 2456ca828f Merge branch 'bluesky-social:main' into patch-3 2024-03-12 16:24:32 +09:00
Samuel Newman 1c9debed9a move imports to top 2024-03-12 00:30:02 +00:00
Samuel Newman f3619cf9f9 move button inline if viewport is small (vertically) 2024-03-12 00:21:48 +00:00
Paul Frazee c1c7a55fd5 Update po files 2024-03-11 14:26:18 -07:00
Hailey c83fe94f41 Fix onboarding FeedCard text overflow (#3176) 2024-03-11 12:57:37 -07:00
Hailey 3a6e58ba6e Fix lineHeight problems on native with new Text (#3175) 2024-03-11 12:54:51 -07:00
Hailey a289e6bce5 only include localhost:19006 as intent filter on android (#3167) 2024-03-11 11:57:23 -07:00
Hailey e4a1069b43 await share before closing menu (#3173) 2024-03-11 10:42:37 -07:00
Jan-Olof Eriksson 4a2251f48b Merge branch 'bluesky-social:main' into main 2024-03-11 14:52:33 +02:00
Alice 596e744d41 Fix button widths in the Settings on web (#3165)
* Fix button widths in the Settings on Web
Had to set `box-sizing: content-box` because
in certain translations the buttons would
become multiline.

Fixes #3060.

* you know what. whatever. i didn't want to be able to change box-sizing anyways. i'm perfectly happy with this workaround.

* ...

* flex-grow: 1
2024-03-10 15:00:51 -07:00
Minseo Lee 236223d02a Merge branch 'bluesky-social:main' into patch-3 2024-03-10 22:50:48 +09:00
Eric Bailey 1aaed1cc0d Fix font styles on web (#3162) 2024-03-09 15:42:42 -08:00
Alice f6685419d8 Fix aria-label on the Share button (#3159)
It had the same one as the Like button for some reason
2024-03-09 13:07:56 -08:00
Eric Bailey 594958c6dc Fix RSS URLs treated as internal (#3156)
* Fix RSS URLs treated as internal

* Add utils to patch relative RSS external links

* modify router to support multiple paths

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-03-09 10:35:23 -06:00
dan aad8c080ed Poll Statsig for config changes (#3158) 2024-03-08 16:29:31 -08:00
Hailey 7899a6fd09 Fix missing check for invite code (#3157) 2024-03-08 15:25:33 -08:00
Hailey 62e57c3b08 Adjustments to ALF prompt buttons (Dialogs Pt. 2) (#3144)
* Improve a11y on ios

* Format

* Remove android

* Fix android

* small adjustment to buttons in prompt

* full width below gtmobile

* Revert some changes

* use sharedvalue for `importantForAccessibility`

* add back `isOpen`

* fix some more types

* small adjustment to buttons in prompt

* full width below gtmobile

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2024-03-08 14:43:28 -08:00
Hailey 8ee325e73d Make ALF prompt scrollable for accessibility (#3150)
* make alf prompt scrollable

* padding
2024-03-08 14:31:50 -08:00
Hailey 0f9f08b1ef Fix reactivity of dialogs (Dialogs Pt. 1) (#3146)
* Improve a11y on ios

* Format

* Remove android

* Fix android

* Revert some changes

* use sharedvalue for `importantForAccessibility`

* add back `isOpen`

* fix some more types

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2024-03-08 14:31:24 -08:00
Eric Bailey 8f623c3bdf Refactor PostDropdownBtn to use new Menu (#3112)
* Refactor PostDropdownBtn

(cherry picked from commit 0adf6cb75e3d4b7c1630cf6153c0d7e289e1b859)

* Update icons

(cherry picked from commit ac89ef9b28721c00736b1388455f3f5f092de0ad)

* Port over fixes

* fix scrollbar disappearing

* Try CSS solution

* Disable arrow for now

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-03-08 14:45:59 -06:00
dan dd86d0964d Enable gating and experimentation on native, send init event (#3149)
* Add the mobile fork

* Add init event
2024-03-08 04:33:42 +00:00
Hailey 31826633cb rm waitlist modal, button during sign up (#3148) 2024-03-07 20:14:24 -08:00
dan c8e0fa9c97 Mark bundle start time on web (#3147)
* Mark bundle start time on web

* TS
2024-03-08 04:13:36 +00:00
Hailey 1cf49517b5 Allow all encoding for hashtags in URL (#3131) 2024-03-07 09:04:02 -08:00
Minseo Lee e74c46e9ab Merge branch 'main' into patch-3 2024-03-07 11:19:50 +09:00
Hailey 8b0e575f64 Adjust FlatList performance in main feeds (#3134)
* adjust flatlist perf settings

* calculate initial num to render based on screen height

* adjust window size

* don't react to screen height changes
2024-03-06 15:33:23 -08:00
Hailey 357b61d0a5 version bump (wait for release) (#3095) 2024-03-06 15:24:37 -08:00
Hailey d5b0a33ae3 Run extract and compile when building for web (#3132) 2024-03-06 12:27:49 -08:00
Hailey 800eecbfe4 fix internal links showing up as external pt2 (#3129)
* fix internal links showing up as external pt2

* fix internal links showing up as external pt2
2024-03-06 11:40:10 -08:00
Hailey 4e6020b88c fix internal links showing up as external (#3128)
* fix internal links showing up as external

* fix internal links showing up as external
2024-03-06 11:34:48 -08:00
Hailey 2d9a5db967 Switch date picker libraries (#3126)
* Revert other base PR changes, switch date picker library

rm expo-linear-gradient

Revert "remove unused packages, switch to `expo-linear-gradient`"

This reverts commit 20a0ffcf

Revert "upgrade expo deps"

This reverts commit a43dca92ca.

Revert other library changes

This reverts commit 5219f66e

Revert "re-add normalize-url"

This reverts commit 654019c4ba.

Revert "add `expo-haptics`"

This reverts commit ff3a0399b1.

Revert "add `expo-clipboard`"

This reverts commit 440ae91632.

Revert "add `expo-file-system`"

This reverts commit c0fe0c9453.

fix android alf

use modal on android

migrate to `react-native-date-picker`

rm `@reactnativecommunity/datetimepicker`

add `react-native-date-picker`

add `expo-file-system`

add `expo-clipboard`

add `expo-haptics`

re-add normalize-url

rm blur view

upgrade expo deps

remove unused packages, switch to `expo-linear-gradient`

* rm old library

* Use UTC for dates

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2024-03-06 13:13:03 -06:00
Hailey b8f36a8bca Fix double border on web hashtag list (#3091)
* add `topBorder` to Views.tsx

* fix double border
2024-03-06 11:02:48 -08:00
Jake Gold 7a592d8140 bskyweb: remove waitlist email endpoint (#3127) 2024-03-06 10:33:41 -08:00
Eiichi Yoshikawa f61d1e1f94 Apply notification icon settings of FCM on Android (#3113)
Co-authored-by: Hailey <me@haileyok.com>
2024-03-06 10:24:08 -08:00
dan 5b8d116e33 By default, hide replies to people you don't follow in the Following feed (#3124)
* Show replies from followed by default

* Update @atproto/api
2024-03-06 17:27:59 +00:00
dan 2e0f806441 Fix jumpy moderation icon on desktop (#3125) 2024-03-06 16:12:37 +00:00
Minseo Lee f3db23a3b1 Merge branch 'bluesky-social:main' into patch-3 2024-03-06 19:38:48 +09:00
Minseo Lee 0c6a82062f Update src/view/screens/Settings/index.tsx
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2024-03-06 19:38:23 +09:00
Minseo Lee 4a858bacd6 Update src/view/screens/Settings/index.tsx
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2024-03-06 19:38:13 +09:00
dan eb298d2e60 Initial feature gating and A/B testing integration (#3122)
* Add statsig dependency

* Add SDK provider

* Move to separate file, add tier and hashing

* Disable local storage for now

* Add initial gate testing fixture

* Fork for web just in case

* More WIP

* wip

* Rm test gate

* Add shim on native

* Clarify
2024-03-06 05:55:34 +00:00
Eiichi Yoshikawa 26fc0cf66d Improve splash display on android (#3105)
* Set window background during startup on android

* Set statusbar color to transparent and fine-tune it
2024-03-05 21:28:27 -08:00
Eric Bailey 317e0cda7a Add Menu component (#3097)
* Add POC menu abstraction

* Better platform handling

* Remove ignore

* Add some menu items

* Add controlled dropdown

* Pass through a11y props

* Ignore uninitialized context

* Tweaks

* Usability improvements

* Rename handlers to props

* Add radix comment

* Ignore known type

* Remove todo

* Move storybook item

* Improve Group matching

* Adjust theming
2024-03-05 21:15:42 -06:00
Hailey e721f84a2c add newArchEnabled: false flag to app.config.js (#3115) 2024-03-05 18:55:23 -08:00
Hailey 57854e6fc2 use onClose instead of onChange for close callback (#3116) 2024-03-05 16:55:20 -08:00
dan 20b88f43e2 Hide feeds topbar on scroll (#3108) 2024-03-05 16:45:08 +00:00
Jan-Olof Eriksson cbc65247ae Add files via upload 2024-03-04 19:09:47 +02:00
Jan-Olof Eriksson 521530eeb6 Update messages.po
Updated latest strings
2024-03-04 19:05:03 +02:00
Jan-Olof Eriksson c52635c578 Update messages.po 2024-03-04 19:04:04 +02:00
Jan-Olof Eriksson e8bc44e166 Update messages.po
Updated new strings
2024-03-04 19:02:48 +02:00
Jan-Olof Eriksson e724ab8123 Merge branch 'bluesky-social:main' into main 2024-03-04 18:11:00 +02:00
Gabriella 4022598692 Update messages.po
Updated two expression
2024-03-03 12:47:39 +01:00
Minseo Lee d0438b6b28 Update Lists.tsx 2024-03-02 13:26:21 +09:00
Minseo Lee ab2b454be8 Merge branch 'bluesky-social:main' into patch-3 2024-03-02 13:04:51 +09:00
Gabriella fb8a7e37f4 Update messages.po
Changed a longer sentence for a shorter version in order to fit on the app screen.
2024-03-01 10:52:50 +01:00
Gabriella ca20643ff8 Update messages.po
sentence updated
2024-02-29 14:14:46 +01:00
Jan-Olof Eriksson 963a44ab87 Merge branch 'bluesky-social:main' into main 2024-02-29 11:55:03 +02:00
Minseo Lee 537ae578d6 Update Search.tsx 2024-02-29 14:25:52 +09:00
Minseo Lee 200c4c1d37 Merge branch 'bluesky-social:main' into patch-3 2024-02-29 13:05:45 +09:00
Minseo Lee a1127bfcfc Merge branch 'main' into patch-3 2024-02-29 09:51:56 +09:00
Minseo Lee b723c4ca7c Merge branch 'bluesky-social:main' into patch-3 2024-02-28 16:26:19 +09:00
Minseo Lee 3767e76390 Merge branch 'bluesky-social:main' into patch-3 2024-02-28 13:03:55 +09:00
Minseo Lee 41e69651f9 Update ChangePassword.tsx 2024-02-27 19:17:58 +09:00
Minseo Lee b2605d3908 Update ChangePassword.tsx 2024-02-27 19:17:47 +09:00
Gabriella eca1333b7c Update messages.po
Corrected small typo
2024-02-27 09:55:51 +01:00
Gabriella 505787e437 Update messages.po
Translation updated with minor corrections
2024-02-27 09:44:12 +01:00
Minseo Lee 8d394a3541 Merge branch 'main' into patch-3 2024-02-27 14:39:41 +09:00
Jan-Olof Eriksson 1f95628475 Merge branch 'bluesky-social:main' into main 2024-02-24 16:37:08 +02:00
Minseo Lee 860b5307ad Update NotFound.tsx 2024-02-24 18:29:12 +09:00
Minseo Lee 5809dd0624 accessibilityLabel 2024-02-24 18:29:01 +09:00
Minseo Lee 7a920d2eb4 Update NotFound.tsx 2024-02-24 18:25:29 +09:00
Minseo Lee 25d7e2449f Update NotFound.tsx 2024-02-24 18:23:14 +09:00
Minseo Lee 89c65c856e Merge branch 'bluesky-social:main' into patch-3 2024-02-24 18:23:03 +09:00
Minseo Lee c2d87b8075 Merge branch 'bluesky-social:main' into patch-3 2024-02-22 09:37:09 +09:00
Jan-Olof Eriksson 38fd4282f8 Merge branch 'bluesky-social:main' into main 2024-02-21 13:22:13 +02:00
Jan-Olof Eriksson 1269e76071 Update Finnish to lingui.config.js
Added Finnish localization
2024-02-19 08:33:20 +02:00
Jan-Olof Eriksson a612aa276c Update i18n.ts
Fixed typo in line 9
2024-02-19 08:09:49 +02:00
Jan-Olof Eriksson f78b55a235 Update languages.ts
Typo
2024-02-19 08:07:18 +02:00
Jan-Olof Eriksson f78e4c2433 Update i18n.web.ts
Added Finnish
2024-02-19 07:43:24 +02:00
Jan-Olof Eriksson 1cfa4aabbd Update i18n.ts
Added Finnish
2024-02-19 07:42:07 +02:00
Jan-Olof Eriksson 76a5e3af71 Update helpers.ts
Added Finnish
2024-02-19 07:39:46 +02:00
Jan-Olof Eriksson 003e18b94d Update languages.ts
Suomi - Finnish
2024-02-19 07:33:17 +02:00
Jan-Olof Eriksson 93318bef05 Update languages.ts
Added Finnish - Suomi
2024-02-19 07:20:57 +02:00
Minseo Lee 6d422bb583 Merge branch 'main' into patch-3 2024-02-19 14:17:59 +09:00
Hailey dcf9fa5e11 Merge branch 'main' into fork/main
# Conflicts:
#	src/locale/locales/it/messages.po
2024-02-18 14:44:48 -08:00
Jan-Olof Eriksson c97d971f09 Update messages.po 2024-02-19 00:12:13 +02:00
Jan-Olof Eriksson 79921b65f8 Update messages.po 2024-02-18 23:47:34 +02:00
Jan-Olof Eriksson 91a4e06878 Update messages.po
Minor changes. (Handle=käyttäjätunnus)
2024-02-18 23:41:58 +02:00
Jan-Olof Eriksson 123c73029d Update messages.po
Some fixes.
2024-02-18 23:21:22 +02:00
jaoler 00103e8e58 Create messages.po
Finnish tranlation for Bluesky
2024-02-18 20:24:26 +02:00
Gabriella 31b7f76015 Update messages.po
Additional grammatical correction related to subject gender
2024-02-18 09:56:14 +01:00
Gabriella 95bc461932 Update messages.po
Updated Nozioni di Base to Preferenze, which has a more logical contest
2024-02-18 09:48:34 +01:00
Gabriella 89f3135ebc Update messages.po
Additional grammatical changes
2024-02-18 09:34:21 +01:00
Gabriella 74895e18c3 Update messages.po
grammatical changes related to the subject
2024-02-18 09:26:02 +01:00
Gabriella 65606b3929 Update messages.po
Changed the tense of a grammatical sentence to present
2024-02-18 09:07:26 +01:00
Gabriella 86df874dd3 Update messages.po
changes some wordings, corrected a typo
2024-02-17 10:18:44 +01:00
Gabriella N 646a685f26 Update messages.po
Changed translation for the word "Following" in the header section
2024-02-17 09:40:59 +01:00
Gabriella N 87825f19ec Update messages.po
Corrected a small grammatical error
2024-02-16 20:06:57 +01:00
Gabriella N 4ab7909f8b Update messages.po 2024-02-16 15:30:12 +01:00
Gabriella N 2b02976923 Add files via upload 2024-02-16 15:27:30 +01:00
Minseo Lee b3e6f0f29d Update ExportCarDialog.tsx 2024-02-16 10:16:56 +09:00
Minseo Lee b8b5d24fb9 Update Moderation.tsx 2024-02-16 10:08:28 +09:00
Minseo Lee 36d2b02a9e Update index.tsx 2024-02-16 10:05:43 +09:00
Minseo Lee 312f54cfc0 Update Moderation.tsx 2024-02-16 10:05:40 +09:00
Minseo Lee 46ee6a0748 Update LanguageSettings.tsx 2024-02-16 09:00:59 +09:00
Minseo Lee c15b785016 Merge branch 'bluesky-social:main' into patch-3 2024-02-16 08:58:46 +09:00
Minseo Lee 880149c05e Update ProfileFeed.tsx 2024-02-15 09:46:30 +09:00
Minseo Lee cb4092c959 Update index.tsx 2024-02-15 09:41:51 +09:00
Minseo Lee c94e5f81a0 Update index.tsx 2024-02-15 09:39:29 +09:00
Minseo Lee 2823e8aa47 Update src/view/screens/Moderation.tsx
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2024-02-15 09:19:54 +09:00
Minseo Lee b0cdaf71dd Update src/view/com/modals/LinkWarning.tsx
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2024-02-15 09:19:46 +09:00
Minseo Lee 83cb04b691 Update src/view/com/modals/LinkWarning.tsx
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2024-02-15 09:19:40 +09:00
Minseo Lee 3f1a85198a Update src/view/screens/Settings/index.tsx
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2024-02-15 09:19:28 +09:00
Minseo Lee a7661b1f6f Update src/view/screens/Settings/index.tsx
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2024-02-15 09:19:22 +09:00
Minseo Lee 44259948cc Update Drawer.tsx 2024-02-14 16:14:29 +09:00
Minseo Lee 3f5bdd8be3 Mark more texts for localization 2024-02-14 11:06:37 +09:00
395 changed files with 49560 additions and 25703 deletions
+47 -1
View File
@@ -7,10 +7,52 @@ module.exports = {
'prettier',
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'detox', 'react', 'lingui'],
plugins: [
'@typescript-eslint',
'detox',
'react',
'lingui',
'simple-import-sort',
],
rules: {
'react/no-unescaped-entities': 0,
'react-native/no-inline-styles': 0,
'simple-import-sort/imports': [
'warn',
{
groups: [
// Side effect imports.
['^\\u0000'],
// Node.js builtins prefixed with `node:`.
['^node:'],
// Packages.
// Things that start with a letter (or digit or underscore), or `@` followed by a letter.
// React/React Native priortized, followed by expo
// Followed by all packages excluding unprefixed relative ones
[
'^(react\\/(.*)$)|^(react$)|^(react-native(.*)$)',
'^(expo(.*)$)|^(expo$)',
'^(?!(?:alf|components|lib|locale|logger|platform|screens|state|view)(?:$|\\/))@?\\w',
],
// Relative imports.
// Ideally, anything that starts with a dot or #
// due to unprefixed relative imports being used, we whitelist the relative paths we use
// (?:$|\\/) matches end of string or /
[
'^(?:#\\/)?(?:lib|state|logger|platform|locale)(?:$|\\/)',
'^(?:#\\/)?view(?:$|\\/)',
'^(?:#\\/)?screens(?:$|\\/)',
'^(?:#\\/)?alf(?:$|\\/)',
'^(?:#\\/)?components(?:$|\\/)',
'^#\\/',
'^\\.',
],
// anything else - hopefully we don't have any of these
['^'],
],
},
],
'simple-import-sort/exports': 'warn',
},
ignorePatterns: [
'**/__mocks__/*.ts',
@@ -31,4 +73,8 @@ module.exports = {
settings: {
componentWrapperFunctions: ['observer'],
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 'latest',
},
}
@@ -3,8 +3,8 @@ on:
push:
branches:
- main
- traffic-reduction
- respect-optout-for-embeds
- 3p-moderators
env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
@@ -0,0 +1,55 @@
---
name: Bundle and Deploy EAS Update
on:
workflow_dispatch:
inputs:
runtimeVersion:
type: string
description: Runtime version (in x.x.x format) that this update is for
required: true
jobs:
bundleDeploy:
name: Bundle and Deploy EAS Update
runs-on: ubuntu-latest
steps:
- name: 🧐 Validate version
run: |
[[ "${{ github.event.inputs.runtimeVersion }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1
- name: ⬇️ Checkout
uses: actions/checkout@v4
- name: 🔧 Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: yarn
- name: ⚙️ Install Dependencies
run: yarn install
- name: 🪛 Install jq
uses: dcarbone/install-jq-action@v2
- name: ⛏️ Setup Expo
run: yarn global add eas-cli-local-build-plugin
- name: 🔤 Compile Translations
run: yarn intl:build
- name: ✏️ Write environment variables
run: |
export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
echo "${{ secrets.ENV_TOKEN }}" > .env
echo "$json" > google-services.json
- name: 🏗️ Create Bundle
run: yarn export
- name: 📦 Package Bundle and 🚀 Deploy
run: yarn make-deploy-bundle
env:
DENIS_API_KEY: ${{ secrets.DENIS_API_KEY }}
RUNTIME_VERSION: ${{ github.event.inputs.runtimeVersion }}
+185
View File
@@ -0,0 +1,185 @@
# Credit https://github.com/expo/expo
# https://github.com/expo/expo/blob/main/.github/workflows/pr-labeler.yml
---
name: PR labeler
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize]
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test-suite-fingerprint:
runs-on: ubuntu-22.04
if: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' }}
# REQUIRED: limit concurrency when pushing main(default) branch to prevent conflict for this action to update its fingerprint database
concurrency: fingerprint-${{ github.event_name != 'pull_request' && 'main' || github.run_id }}
permissions:
# REQUIRED: Allow comments of PRs
pull-requests: write
# REQUIRED: Allow updating fingerprint in acton caches
actions: write
steps:
- name: ⬇️ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 100
- name: ⬇️ Fetch commits from base branch
run: git fetch origin main:main --depth 100
if: github.event_name == 'pull_request'
- name: 🔧 Setup Node
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: yarn
- name: ⚙️ Install Dependencies
run: yarn install
- name: Get the base commit
id: base-commit
run: |
# Since we limit this pr-labeler workflow only triggered from limited paths, we should use custom base commit
echo base-commit=$(git log -n 1 main --pretty=format:'%H') >> "$GITHUB_OUTPUT"
- name: 📷 Check fingerprint
id: fingerprint
uses: expo/expo-github-action/fingerprint@main
with:
previous-git-commit: ${{ steps.base-commit.outputs.base-commit }}
- name: 👀 Debug fingerprint
run: |
echo "previousGitCommit=${{ steps.fingerprint.outputs.previous-git-commit }} currentGitCommit=${{ steps.fingerprint.outputs.current-git-commit }}"
echo "isPreviousFingerprintEmpty=${{ steps.fingerprint.outputs.previous-fingerprint == '' }}"
- name: 🏷️ Labeling PR
uses: actions/github-script@v6
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff == '[]' }}
with:
script: |
try {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: ['bot: fingerprint changed']
})
} catch (e) {
if (e.status != 404) {
throw e;
}
}
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['bot: fingerprint compatible']
})
- name: 🏷️ Labeling PR
uses: actions/github-script@v6
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' }}
with:
script: |
try {
await github.rest.issues.removeLabel({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
name: ['bot: fingerprint compatible']
})
} catch (e) {
if (e.status != 404) {
throw e;
}
}
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['bot: fingerprint changed']
})
- name: 🔍 Find old comment if it exists
uses: peter-evans/find-comment@v2
if: ${{ github.event_name == 'pull_request' }}
id: old_comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: <!-- pr-labeler comment -->
- name: 💬 Add comment with fingerprint
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' && steps.old_comment.outputs.comment-id == '' }}
uses: actions/github-script@v6
with:
script: |
const diff = JSON.stringify(${{ steps.fingerprint.outputs.fingerprint-diff}}, null, 2);
const body = `<!-- pr-labeler comment -->
The Pull Request introduced fingerprint changes against the base commit: ${{ steps.fingerprint.outputs.previous-git-commit }}
<details><summary>Fingerprint diff</summary>
\`\`\`json
${diff}
\`\`\`
</details>
---
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
- name: 💬 Update comment with fingerprint
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff != '[]' && steps.old_comment.outputs.comment-id != '' }}
uses: actions/github-script@v6
with:
script: |
const diff = JSON.stringify(${{ steps.fingerprint.outputs.fingerprint-diff}}, null, 2);
const body = `<!-- pr-labeler comment -->
The Pull Request introduced fingerprint changes against the base commit: ${{ steps.fingerprint.outputs.previous-git-commit }}
<details><summary>Fingerprint diff</summary>
\`\`\`json
${diff}
\`\`\`
</details>
---
*Generated by [PR labeler](https://github.com/expo/expo/actions/workflows/pr-labeler.yml) 🤖*
`;
github.rest.issues.updateComment({
issue_number: context.issue.number,
comment_id: '${{ steps.old_comment.outputs.comment-id }}',
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
- name: 💬 Delete comment with fingerprint
if: ${{ github.event_name == 'pull_request' && steps.fingerprint.outputs.fingerprint-diff == '[]' && steps.old_comment.outputs.comment-id != '' }}
uses: actions/github-script@v6
with:
script: |
github.rest.issues.deleteComment({
issue_number: context.issue.number,
comment_id: '${{ steps.old_comment.outputs.comment-id }}',
owner: context.repo.owner,
repo: context.repo.repo,
});
+1 -1
View File
@@ -31,7 +31,7 @@ RUN \. "$NVM_DIR/nvm.sh" && \
nvm use $NODE_VERSION && \
npm install --global yarn && \
yarn && \
yarn intl:compile && \
yarn intl:build && \
yarn build-web
# DEBUG
+2 -2
View File
@@ -64,7 +64,7 @@ describe('Curate lists', () => {
await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Library')).tap()
await element(by.text('Upload from Library')).tap()
await sleep(3e3)
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
@@ -81,7 +81,7 @@ describe('Curate lists', () => {
await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Remove')).tap()
await element(by.text('Remove Avatar')).tap()
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
await expect(element(by.id('userAvatarFallback'))).toExist()
+1 -1
View File
@@ -17,7 +17,7 @@ describe('Home screen', () => {
it('Can go to feeds page using feeds button in tab bar', async () => {
await element(by.id('homeScreenFeedTabs-Feeds ✨')).tap()
await expect(element(by.text('Discover new feeds'))).toBeVisible()
await expect(element(by.text('Discover New Feeds'))).toBeVisible()
})
it('Feeds button disappears after pinning a feed', async () => {
+4 -4
View File
@@ -70,10 +70,10 @@ describe('Profile screen', () => {
await element(by.id('profileHeaderEditProfileButton')).tap()
await expect(element(by.id('editProfileModal'))).toBeVisible()
await element(by.id('changeBannerBtn')).tap()
await element(by.text('Library')).tap()
await element(by.text('Upload from Library')).tap()
await sleep(3e3)
await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Library')).tap()
await element(by.text('Upload from Library')).tap()
await sleep(3e3)
await element(by.id('editProfileSaveBtn')).tap()
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
@@ -87,9 +87,9 @@ describe('Profile screen', () => {
await element(by.id('profileHeaderEditProfileButton')).tap()
await expect(element(by.id('editProfileModal'))).toBeVisible()
await element(by.id('changeBannerBtn')).tap()
await element(by.text('Remove')).tap()
await element(by.text('Remove Banner')).tap()
await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Remove')).tap()
await element(by.text('Remove Avatar')).tap()
await element(by.id('editProfileSaveBtn')).tap()
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
await expect(element(by.id('userBannerFallback'))).toExist()
@@ -12,6 +12,7 @@ describe('Create account', () => {
})
it('I can create a new account with text verification', async () => {
console.log('SERVICE IS', service)
await element(by.id('e2eOpenLoggedOutView')).tap()
await element(by.id('createAccountButton')).tap()
@@ -28,16 +29,17 @@ describe('Create account', () => {
await device.takeScreenshot('4- entered account details')
await element(by.id('nextBtn')).tap()
await element(by.id('phoneInput')).typeText('8042221111')
await element(by.id('requestCodeBtn')).tap()
await device.takeScreenshot('5- requested code')
await element(by.id('codeInput')).typeText('000000')
await device.takeScreenshot('6- entered code')
await element(by.id('handleInput')).typeText('text-verification-test')
await device.takeScreenshot('5- entered handle')
await element(by.id('nextBtn')).tap()
await element(by.id('handleInput')).typeText('text-verification-test')
await device.takeScreenshot('7- entered handle')
await element(by.id('phoneInput')).typeText('8042221111')
await element(by.id('requestCodeBtn')).tap()
await device.takeScreenshot('6- requested code')
await element(by.id('codeInput')).typeText('000000')
await device.takeScreenshot('7- entered code')
await element(by.id('nextBtn')).tap()
await element(by.id('nextBtn')).tap()
+38
View File
@@ -4,6 +4,7 @@ import {
linkRequiresWarning,
isPossiblyAUrl,
splitApexDomain,
isTrustedUrl,
} from '../../../src/lib/strings/url-helpers'
describe('linkRequiresWarning', () => {
@@ -74,6 +75,10 @@ describe('linkRequiresWarning', () => {
// bad uri inputs, default to true
['', '', true],
['example.com', 'example.com', true],
['/profile', 'Username', false],
['#', 'Show More', false],
['https://docs.bsky.app', 'https://docs.bsky.app', false],
['https://bsky.app/compose/intent?text=test', 'Compose a post', false],
]
it.each(cases)(
@@ -139,3 +144,36 @@ describe('splitApexDomain', () => {
},
)
})
describe('isTrustedUrl', () => {
const cases = [
['#', true],
['#profile', true],
['/', true],
['/profile', true],
['/profile/', true],
['/profile/bob.test', true],
['https://bsky.app', true],
['https://bsky.app/', true],
['https://bsky.app/profile/bob.test', true],
['https://www.bsky.app', true],
['https://www.bsky.app/', true],
['https://docs.bsky.app', true],
['https://bsky.social', true],
['https://bsky.social/blog', true],
['https://blueskyweb.xyz', true],
['https://blueskyweb.zendesk.com', true],
['http://bsky.app', true],
['http://bsky.social', true],
['http://blueskyweb.xyz', true],
['http://blueskyweb.zendesk.com', true],
['https://google.com', false],
['https://docs.google.com', false],
['https://google.com/#', false],
]
it.each(cases)('given input uri %p, returns %p', (str, expected) => {
const output = isTrustedUrl(str)
expect(output).toEqual(expected)
})
})
+24 -6
View File
@@ -11,6 +11,17 @@ const DARK_SPLASH_CONFIG = {
resizeMode: 'cover',
}
const SPLASH_CONFIG_ANDROID = {
backgroundColor: '#0c7cff',
image: './assets/splash.png',
resizeMode: 'cover',
}
const DARK_SPLASH_CONFIG_ANDROID = {
backgroundColor: '#0f141b',
image: './assets/splash-dark.png',
resizeMode: 'cover',
}
module.exports = function (config) {
/**
* App version number. Should be incremented as part of a release cycle.
@@ -29,6 +40,8 @@ module.exports = function (config) {
? process.env.BSKY_ANDROID_VERSION_CODE
: process.env.BSKY_IOS_BUILD_NUMBER
const IS_DEV = process.env.EXPO_PUBLIC_ENV === 'development'
return {
expo: {
version: VERSION,
@@ -70,8 +83,8 @@ module.exports = function (config) {
},
},
androidStatusBar: {
barStyle: 'dark-content',
backgroundColor: '#ffffff',
barStyle: 'light-content',
backgroundColor: '#00000000',
},
android: {
icon: './assets/icon.png',
@@ -92,7 +105,7 @@ module.exports = function (config) {
scheme: 'https',
host: 'bsky.app',
},
{
IS_DEV && {
scheme: 'http',
host: 'localhost:19006',
},
@@ -101,8 +114,8 @@ module.exports = function (config) {
},
],
splash: {
...SPLASH_CONFIG,
dark: DARK_SPLASH_CONFIG,
...SPLASH_CONFIG_ANDROID,
dark: DARK_SPLASH_CONFIG_ANDROID,
},
},
web: {
@@ -121,12 +134,14 @@ module.exports = function (config) {
{
ios: {
deploymentTarget: '13.4',
newArchEnabled: false,
},
android: {
compileSdkVersion: 34,
targetSdkVersion: 34,
buildToolsVersion: '34.0.0',
kotlinVersion: '1.8.0',
newArchEnabled: false,
},
},
],
@@ -140,10 +155,13 @@ module.exports = function (config) {
'expo-notifications',
{
icon: './assets/icon-android-notification.png',
color: '#ffffff',
color: '#1185fe',
},
],
'./plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js',
'./plugins/withAndroidStylesWindowBackgroundPlugin.js',
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
'./plugins/shareExtension/withShareExtensions.js',
].filter(Boolean),
extra: {
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M4.213 6.886c-.673-1.35.334-2.889 1.806-2.889H17.98c1.472 0 2.479 1.539 1.806 2.89l-5.982 11.997c-.74 1.484-2.87 1.484-3.61 0L4.213 6.886Z"/></svg>

After

Width:  |  Height:  |  Size: 240 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 5a1 1 0 0 0 0 2h18a1 1 0 1 0 0-2H3Zm-1 7a1 1 0 0 1 1-1h18a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm0 6a1 1 0 0 1 1-1h18a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 286 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M5.002 17.036V5h14v12.036h-3.986a1 1 0 0 0-.639.23l-2.375 1.968-2.344-1.965a1 1 0 0 0-.643-.233H5.002ZM20.002 3h-16a1 1 0 0 0-1 1v14.036a1 1 0 0 0 1 1h4.65l2.704 2.266a1 1 0 0 0 1.28.004l2.74-2.27h4.626a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1Zm-7.878 3.663c-1.39 0-2.5 1.135-2.5 2.515a1 1 0 0 0 2 0c0-.294.232-.515.5-.515a.507.507 0 0 1 .489.6.174.174 0 0 1-.027.048 1.1 1.1 0 0 1-.267.226c-.508.345-1.128.923-1.286 1.978a1 1 0 1 0 1.978.297.762.762 0 0 1 .14-.359c.063-.086.155-.169.293-.262.436-.297 1.18-.885 1.18-2.013 0-1.38-1.11-2.515-2.5-2.515ZM12 15.75a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 738 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8 2a1 1 0 0 1 1 1v1h6V3a1 1 0 1 1 2 0v1h2a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2V3a1 1 0 0 1 1-1ZM5 6v3h14V6H5Zm14 5H5v8h14v-8Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 296 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8.371 3.89A2 2 0 0 1 10.035 3h3.93a2 2 0 0 1 1.664.89L17.035 6H20a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.965L8.37 3.89ZM12 9a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 330 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8.371 3.89A2 2 0 0 1 10.035 3h3.93a2 2 0 0 1 1.664.89L17.035 6H20a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.965L8.37 3.89ZM13.965 5h-3.93L8.63 7.11A2 2 0 0 1 6.965 8H4v11h16V8h-2.965a2 2 0 0 1-1.664-.89L13.965 5ZM12 11a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm-4 2a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 447 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3.293 8.293a1 1 0 0 1 1.414 0L12 15.586l7.293-7.293a1 1 0 1 1 1.414 1.414l-8 8a1 1 0 0 1-1.414 0l-8-8a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 263 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 6a1 1 0 0 1 .707.293l8 8a1 1 0 0 1-1.414 1.414L12 8.414l-7.293 7.293a1 1 0 0 1-1.414-1.414l8-8A1 1 0 0 1 12 6Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 256 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a8 8 0 0 0-6.32 12.906L16.906 5.68A7.962 7.962 0 0 0 12 4Zm6.32 3.094L7.094 18.32A8 8 0 0 0 18.32 7.094ZM2 12C2 6.477 6.477 2 12 2a9.972 9.972 0 0 1 7.071 2.929A9.972 9.972 0 0 1 22 12c0 5.523-4.477 10-10 10a9.972 9.972 0 0 1-7.071-2.929A9.972 9.972 0 0 1 2 12Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 409 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M2 12a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm16 0a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm-6-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 245 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4.568 4h14.864c.252 0 .498 0 .706.017.229.019.499.063.77.201a2 2 0 0 1 .874.874c.138.271.182.541.201.77.017.208.017.454.017.706v10.864c0 .252 0 .498-.017.706a2.022 2.022 0 0 1-.201.77 2 2 0 0 1-.874.874 2.022 2.022 0 0 1-.77.201c-.208.017-.454.017-.706.017H4.568c-.252 0-.498 0-.706-.017a2.022 2.022 0 0 1-.77-.201 2 2 0 0 1-.874-.874 2.022 2.022 0 0 1-.201-.77C2 17.93 2 17.684 2 17.432V6.568c0-.252 0-.498.017-.706.019-.229.063-.499.201-.77a2 2 0 0 1 .874-.874c.271-.138.541-.182.77-.201C4.07 4 4.316 4 4.568 4Zm.456 2L12 11.708 18.976 6H5.024ZM20 7.747l-6.733 5.509a2 2 0 0 1-2.534 0L4 7.746V17.4a8.187 8.187 0 0 0 .011.589h.014c.116.01.278.011.575.011h14.8a8.207 8.207 0 0 0 .589-.012v-.013c.01-.116.011-.279.011-.575V7.747Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 871 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v4a1 1 0 0 1-.293.707L15 14.414V20a1 1 0 0 1-.758.97l-4 1A1 1 0 0 1 9 21v-6.586L3.293 8.707A1 1 0 0 1 3 8V4Zm2 1v2.586l5.707 5.707A1 1 0 0 1 11 14v5.72l2-.5V14a1 1 0 0 1 .293-.707L19 7.586V5H5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 371 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4 4a2 2 0 0 1 2-2h13.131c1.598 0 2.55 1.78 1.665 3.11L18.202 9l2.594 3.89c.886 1.33-.067 3.11-1.665 3.11H6v5a1 1 0 1 1-2 0V4Zm2 10h13.131l-2.593-3.89a2 2 0 0 1 0-2.22L19.13 4H6v10Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 323 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8 5a2 2 0 1 0 0 4 2 2 0 0 0 0-4ZM4 7a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm13-1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm-3.5 1.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Zm5.826 7.376c-.919-.779-2.052-1.03-3.1-.787a1 1 0 0 1-.451-1.949c1.671-.386 3.45.028 4.844 1.211 1.397 1.185 2.348 3.084 2.524 5.579a1 1 0 0 1-.997 1.07H18a1 1 0 1 1 0-2h3.007c-.29-1.47-.935-2.49-1.681-3.124ZM3.126 19h9.747c-.61-3.495-2.867-5-4.873-5-2.006 0-4.263 1.505-4.873 5ZM8 12c3.47 0 6.64 2.857 6.998 7.93A1 1 0 0 1 14 21H2a1 1 0 0 1-.998-1.07C1.36 14.857 4.53 12 8 12Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 674 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12.489 21.372c8.528-4.78 10.626-10.47 9.022-14.47-.779-1.941-2.414-3.333-4.342-3.763-1.697-.378-3.552.003-5.169 1.287-1.617-1.284-3.472-1.665-5.17-1.287-1.927.43-3.562 1.822-4.34 3.764-1.605 4 .493 9.69 9.021 14.47a1 1 0 0 0 .978 0Z"/></svg>

After

Width:  |  Height:  |  Size: 336 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M16.734 5.091c-1.238-.276-2.708.047-4.022 1.38a1 1 0 0 1-1.424 0C9.974 5.137 8.504 4.814 7.266 5.09c-1.263.282-2.379 1.206-2.92 2.556C3.33 10.18 4.252 14.84 12 19.348c7.747-4.508 8.67-9.168 7.654-11.7-.541-1.351-1.657-2.275-2.92-2.557Zm4.777 1.812c1.604 4-.494 9.69-9.022 14.47a1 1 0 0 1-.978 0C2.983 16.592.885 10.902 2.49 6.902c.779-1.942 2.414-3.334 4.342-3.764 1.697-.378 3.552.003 5.169 1.286 1.617-1.283 3.472-1.664 5.17-1.286 1.927.43 3.562 1.822 4.34 3.764Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 608 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M7 7a5 5 0 0 1 10 0v2h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h1V7Zm-1 4v9h12v-9H6Zm9-2H9V7a3 3 0 1 1 6 0v2Zm-3 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 327 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M15.586 2.5a2 2 0 0 1 2.828 0L21.5 5.586a2 2 0 0 1 0 2.828l-13 13A2 2 0 0 1 7.086 22H3a1 1 0 0 1-1-1v-4.086a2 2 0 0 1 .586-1.414l13-13ZM17 3.914l-13 13V20h3.086l13-13L17 3.914ZM13 21a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2h-7a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 373 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#000" fill-rule="evenodd" d="M10 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM5.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM16 11a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2h-5a1 1 0 0 1-1-1ZM3.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C1.917 15.521 5.242 12 10 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 17.5 21h-15a1 1 0 0 1-.996-1.094Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 466 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5a6.69 6.69 0 0 1 2.612.51 1 1 0 0 0 .776-1.844A8.687 8.687 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H11a1 1 0 1 0 0-2H5.679Zm14.835-4.857a1 1 0 0 1 .344 1.371l-3 5a1 1 0 0 1-1.458.286l-2-1.5a1 1 0 0 1 1.2-1.6l1.113.835 2.43-4.05a1 1 0 0 1 1.372-.342Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 547 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5 .302 0 .595.018.878.053a1 1 0 0 0 .243-1.985A9.235 9.235 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H12a1 1 0 1 0 0-2H5.679Zm9.614-3.707a1 1 0 0 1 1.414 0L18 16.586l1.293-1.293a1 1 0 0 1 1.414 1.414L19.414 18l1.293 1.293a1 1 0 0 1-1.414 1.414L18 19.414l-1.293 1.293a1 1 0 0 1-1.414-1.414L16.586 18l-1.293-1.293a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 625 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C3.917 15.521 7.242 12 12 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 19.5 21h-15a1 1 0 0 1-.996-1.094Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 410 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M10.25 4a.75.75 0 0 0-.75.75V11a1 1 0 1 1-2 0V6.75a.75.75 0 0 0-1.5 0V14a6 6 0 0 0 12 0V9a2 2 0 0 0-2 2v1.5a1 1 0 0 1-.684.949l-.628.21A2.469 2.469 0 0 0 13 16a1 1 0 1 1-2 0 4.469 4.469 0 0 1 3-4.22V11c0-.703.181-1.364.5-1.938V5.75a.75.75 0 0 0-1.5 0V9a1 1 0 1 1-2 0V4.75a.75.75 0 0 0-.75-.75Zm2.316-.733A2.75 2.75 0 0 1 16.5 5.75v1.54c.463-.187.97-.29 1.5-.29h1a1 1 0 0 1 1 1v6a8 8 0 1 1-16 0V6.75a2.75 2.75 0 0 1 3.571-2.625 2.751 2.751 0 0 1 4.995-.858Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 599 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M11.1 2a1 1 0 0 0-.832.445L8.851 4.57 6.6 4.05a1 1 0 0 0-.932.268l-1.35 1.35a1 1 0 0 0-.267.932l.52 2.251-2.126 1.417A1 1 0 0 0 2 11.1v1.8a1 1 0 0 0 .445.832l2.125 1.417-.52 2.251a1 1 0 0 0 .268.932l1.35 1.35a1 1 0 0 0 .932.267l2.251-.52 1.417 2.126A1 1 0 0 0 11.1 22h1.8a1 1 0 0 0 .832-.445l1.417-2.125 2.251.52a1 1 0 0 0 .932-.268l1.35-1.35a1 1 0 0 0 .267-.932l-.52-2.251 2.126-1.417A1 1 0 0 0 22 12.9v-1.8a1 1 0 0 0-.445-.832L19.43 8.851l.52-2.251a1 1 0 0 0-.268-.932l-1.35-1.35a1 1 0 0 0-.932-.267l-2.251.52-1.417-2.126A1 1 0 0 0 12.9 2h-1.8Zm-.968 4.255L11.635 4h.73l1.503 2.255a1 1 0 0 0 1.057.42l2.385-.551.566.566-.55 2.385a1 1 0 0 0 .42 1.057L20 11.635v.73l-2.255 1.503a1 1 0 0 0-.42 1.057l.551 2.385-.566.566-2.385-.55a1 1 0 0 0-1.057.42L12.365 20h-.73l-1.503-2.255a1 1 0 0 0-1.057-.42l-2.385.551-.566-.566.55-2.385a1 1 0 0 0-.42-1.057L4 12.365v-.73l2.255-1.503a1 1 0 0 0 .42-1.057L6.123 6.69l.566-.566 2.385.55a1 1 0 0 0 1.057-.42ZM8 12a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm4-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M11.675 2.054a1 1 0 0 1 .65 0l8 2.75A1 1 0 0 1 21 5.75v6.162c0 2.807-1.149 4.83-2.813 6.405-1.572 1.488-3.632 2.6-5.555 3.636l-.157.085a1 1 0 0 1-.95 0l-.157-.085c-1.923-1.037-3.983-2.148-5.556-3.636C4.15 16.742 3 14.719 3 11.912V5.75a1 1 0 0 1 .675-.946l8-2.75ZM5 6.464v5.448c0 2.166.851 3.687 2.188 4.952 1.276 1.209 2.964 2.158 4.812 3.157 1.848-1 3.536-1.948 4.813-3.157C18.148 15.6 19 14.078 19 11.912V6.464l-7-2.407-7 2.407Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 572 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12.472 3.118A1 1 0 0 1 13 4v16a1 1 0 0 1-1.555.832L5.697 17H2a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h3.697l5.748-3.832a1 1 0 0 1 1.027-.05ZM11 5.868 6.555 8.833A1 1 0 0 1 6 9H3v6h3a1 1 0 0 1 .555.168L11 18.131V5.87Zm7.364-1.645a1 1 0 0 1 1.414 0A10.969 10.969 0 0 1 23 12c0 3.037-1.232 5.788-3.222 7.778a1 1 0 1 1-1.414-1.414A8.969 8.969 0 0 0 21 12a8.969 8.969 0 0 0-2.636-6.364 1 1 0 0 1 0-1.414Zm-3.182 3.181a1 1 0 0 1 1.414 0A6.483 6.483 0 0 1 18.5 12a6.483 6.483 0 0 1-1.904 4.597 1 1 0 0 1-1.414-1.415A4.483 4.483 0 0 0 16.5 12a4.483 4.483 0 0 0-1.318-3.182 1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 717 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M14 5a1 1 0 1 1 0-2h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V6.414l-7.293 7.293a1 1 0 0 1-1.414-1.414L17.586 5H14ZM3 6a1 1 0 0 1 1-1h5a1 1 0 0 1 0 2H5v12h12v-4a1 1 0 1 1 2 0v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 342 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8 8V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1h-5v5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h5Zm1 8a1 1 0 0 1-1-1v-5H4v10h10v-4H9Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 286 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H4Zm8 12.5c1.253 0 2.197.609 2.674 1.5H9.326c.477-.891 1.42-1.5 2.674-1.5Zm0-2c2.404 0 4.235 1.475 4.822 3.5H20V6H4v12h3.178c.587-2.025 2.418-3.5 4.822-3.5Zm-1.25-3.75a1.25 1.25 0 1 1 2.5 0 1.25 1.25 0 0 1-2.5 0ZM12 7.5a3.25 3.25 0 1 0 0 6.5 3.25 3.25 0 0 0 0-6.5Zm5.75 2a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 527 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" stroke="#000" stroke-linejoin="round" d="M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.498 3.498 0 0 1 5.5 12a3.498 3.498 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.498 3.498 0 0 1 18.5 12a3.5 3.5 0 0 1 1.75-3.032.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z"/></svg>

After

Width:  |  Height:  |  Size: 768 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M7.416 5H3a1 1 0 0 0 0 2h1.064l.938 14.067A1 1 0 0 0 6 22h12a1 1 0 0 0 .998-.933L19.936 7H21a1 1 0 1 0 0-2h-4.416a5 5 0 0 0-9.168 0Zm2.348 0h4.472c-.55-.614-1.348-1-2.236-1-.888 0-1.687.386-2.236 1Zm6.087 2H6.07l.867 13h10.128l.867-13h-2.036a1 1 0 0 1-.044 0ZM10 10a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 508 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12.86 4.494a.995.995 0 0 0-1.72 0L4.14 16.502A.996.996 0 0 0 4.999 18h14.003a.996.996 0 0 0 .86-1.498L12.86 4.494ZM9.413 3.487c1.155-1.983 4.019-1.983 5.174 0l7.002 12.007C22.753 17.491 21.314 20 19.002 20H4.998c-2.312 0-3.751-2.509-2.587-4.506L9.413 3.487ZM12 8.019a1 1 0 0 1 1 1v2.994a1 1 0 1 1-2 0V9.02a1 1 0 0 1 1-1Z" clip-rule="evenodd"/><rect width="2.5" height="2.5" x="10.75" y="13.75" fill="#000" rx="1.25"/></svg>

After

Width:  |  Height:  |  Size: 537 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M11.14 4.494a.995.995 0 0 1 1.72 0l7.001 12.008a.996.996 0 0 1-.86 1.498H4.999a.996.996 0 0 1-.86-1.498L11.14 4.494Zm3.447-1.007c-1.155-1.983-4.019-1.983-5.174 0L2.41 15.494C1.247 17.491 2.686 20 4.998 20h14.004c2.312 0 3.751-2.509 2.587-4.506L14.587 3.487ZM13 9.019a1 1 0 1 0-2 0v2.994a1 1 0 1 0 2 0V9.02Zm-1 4.731a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 503 B

-70
View File
@@ -1,70 +0,0 @@
package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"net/http"
"time"
)
type Mailmodo struct {
httpClient *http.Client
APIKey string
BaseURL string
ListName string
}
func NewMailmodo(apiKey, listName string) *Mailmodo {
return &Mailmodo{
APIKey: apiKey,
BaseURL: "https://api.mailmodo.com/api/v1",
httpClient: &http.Client{},
ListName: listName,
}
}
func (m *Mailmodo) request(ctx context.Context, httpMethod string, apiMethod string, data any) error {
endpoint := fmt.Sprintf("%s/%s", m.BaseURL, apiMethod)
js, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("Mailmodo JSON encoding failed: %w", err)
}
req, err := http.NewRequestWithContext(ctx, httpMethod, endpoint, bytes.NewBuffer(js))
if err != nil {
return fmt.Errorf("Mailmodo HTTP creating request %s %s failed: %w", httpMethod, apiMethod, err)
}
req.Header.Set("mmApiKey", m.APIKey)
req.Header.Set("Content-Type", "application/json")
res, err := m.httpClient.Do(req)
if err != nil {
return fmt.Errorf("Mailmodo HTTP making request %s %s failed: %w", httpMethod, apiMethod, err)
}
defer res.Body.Close()
status := struct {
Success bool `json:"success"`
Message string `json:"message"`
}{}
if err := json.NewDecoder(res.Body).Decode(&status); err != nil {
return fmt.Errorf("Mailmodo HTTP parsing response %s %s failed: %w", httpMethod, apiMethod, err)
}
if !status.Success {
return fmt.Errorf("Mailmodo API response %s %s failed: %s", httpMethod, apiMethod, status.Message)
}
return nil
}
func (m *Mailmodo) AddToList(ctx context.Context, email string) error {
return m.request(ctx, "POST", "addToList", map[string]any{
"listName": m.ListName,
"email": email,
"data": map[string]any{
"email_hashed": fmt.Sprintf("%x", sha256.Sum256([]byte(email))),
},
"created_at": time.Now().UTC().Format(time.RFC3339),
})
}
-12
View File
@@ -40,18 +40,6 @@ func run(args []string) {
// retain old PDS env var for easy transition
EnvVars: []string{"ATP_APPVIEW_HOST", "ATP_PDS_HOST"},
},
&cli.StringFlag{
Name: "mailmodo-api-key",
Usage: "Mailmodo API key",
Required: false,
EnvVars: []string{"MAILMODO_API_KEY"},
},
&cli.StringFlag{
Name: "mailmodo-list-name",
Usage: "Mailmodo contact list to add email addresses to",
Required: false,
EnvVars: []string{"MAILMODO_LIST_NAME"},
},
&cli.StringFlag{
Name: "http-address",
Usage: "Specify the local IP/port to bind to",
+7 -50
View File
@@ -2,11 +2,9 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/fs"
"io/ioutil"
"net/http"
"os"
"os/signal"
@@ -29,25 +27,19 @@ import (
)
type Server struct {
echo *echo.Echo
httpd *http.Server
mailmodo *Mailmodo
xrpcc *xrpc.Client
echo *echo.Echo
httpd *http.Server
xrpcc *xrpc.Client
}
func serve(cctx *cli.Context) error {
debug := cctx.Bool("debug")
httpAddress := cctx.String("http-address")
appviewHost := cctx.String("appview-host")
mailmodoAPIKey := cctx.String("mailmodo-api-key")
mailmodoListName := cctx.String("mailmodo-list-name")
// Echo
e := echo.New()
// Mailmodo client.
mailmodo := NewMailmodo(mailmodoAPIKey, mailmodoListName)
// create a new session (no auth)
xrpcc := &xrpc.Client{
Client: cliutil.NewHttpClient(),
@@ -77,9 +69,8 @@ func serve(cctx *cli.Context) error {
// server
//
server := &Server{
echo: e,
mailmodo: mailmodo,
xrpcc: xrpcc,
echo: e,
xrpcc: xrpcc,
}
// Create the HTTP server.
@@ -197,6 +188,7 @@ func serve(cctx *cli.Context) error {
e.GET("/settings/threads", server.WebGeneric)
e.GET("/settings/external-embeds", server.WebGeneric)
e.GET("/sys/debug", server.WebGeneric)
e.GET("/sys/debug-mod", server.WebGeneric)
e.GET("/sys/log", server.WebGeneric)
e.GET("/support", server.WebGeneric)
e.GET("/support/privacy", server.WebGeneric)
@@ -212,6 +204,7 @@ func serve(cctx *cli.Context) error {
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
e.GET("/profile/:handleOrDID/labeler/liked-by", server.WebGeneric)
// profile RSS feed (DID not handle)
e.GET("/profile/:ident/rss", server.WebProfileRSS)
@@ -221,9 +214,6 @@ func serve(cctx *cli.Context) error {
e.GET("/profile/:handleOrDID/post/:rkey/liked-by", server.WebGeneric)
e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric)
// Mailmodo
e.POST("/api/waitlist", server.apiWaitlist)
// Start the server.
log.Infof("starting server address=%s", httpAddress)
go func() {
@@ -398,36 +388,3 @@ func (srv *Server) WebProfile(c echo.Context) error {
data["requestHost"] = req.Host
return c.Render(http.StatusOK, "profile.html", data)
}
func (srv *Server) apiWaitlist(c echo.Context) error {
type jsonError struct {
Error string `json:"error"`
}
// Read the API request.
type apiRequest struct {
Email string `json:"email"`
}
bodyReader := http.MaxBytesReader(c.Response(), c.Request().Body, 16*1024)
payload, err := ioutil.ReadAll(bodyReader)
if err != nil {
return err
}
var req apiRequest
if err := json.Unmarshal(payload, &req); err != nil {
return c.JSON(http.StatusBadRequest, jsonError{Error: "Invalid API request"})
}
if req.Email == "" {
return c.JSON(http.StatusBadRequest, jsonError{Error: "Please enter a valid email address."})
}
if err := srv.mailmodo.AddToList(c.Request().Context(), req.Email); err != nil {
log.Errorf("adding email to waitlist failed: %s", err)
return c.JSON(http.StatusBadRequest, jsonError{
Error: "Storing email in waitlist failed. Please enter a valid email address.",
})
}
return c.JSON(http.StatusOK, map[string]bool{"success": true})
}
+5 -7
View File
@@ -5,16 +5,14 @@
}
.container {
position: relative;
width: 100%;
height: 0;
padding-bottom: 56.25%;
overflow: hidden;
width: 100vw;
height: 100vh;
}
.video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
width: 100vw;
height: 100vh;
}
</style>
<div class="container"><div class="video" id="player"></div></div>
+5 -1
View File
@@ -43,6 +43,9 @@
height: calc(100% + env(safe-area-inset-top));
scrollbar-gutter: stable both-edges;
}
html, body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif;
}
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
button, input, textarea {
@@ -138,7 +141,7 @@
/* ProseMirror */
.ProseMirror {
font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font: 18px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif;
min-height: 140px;
}
.ProseMirror-dark {
@@ -213,6 +216,7 @@
}
/* NativeDropdown component */
.radix-dropdown-item:focus,
.nativeDropdown-item:focus {
outline: none;
}
+18
View File
@@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC0TCCAbmgAwIBAgIJcMN2yt5KNDqTMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV
BAMTB0JsdWVza3kwHhcNMjQwMzE0MDA1OTU4WhcNMzQwMzE0MDA1OTU4WjASMRAw
DgYDVQQDEwdCbHVlc2t5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
izSAWEc3wRoa3eTBEh/kE9pH0d6jhEGw9GrYfei60MHT1pSq2cTdyUM1yUZchAeW
gFFtqFxX0pfIZQyMlIZbjkaOxOqzWhB0aCsxngnhbSahFwRxkVwTAuonhqIpaLBL
hrCCCQ2IfZUpy8QeasqlTlmvmijuCC34fXxJlxNcj8SqzIZi+civ7U5PMPfIMMnD
tCDIBy1vxMk57m25X2ikcWUFW64qNVLkFAL36xEnmFTL4Ivqpz23gUcUIe1zbesY
jAgDtlwnAE7mU3oagCUDcSuOveT4POhT35Xp3Y/07I68kmXtrPxwd5k0L0zbisEm
poKZ87E2X29BitihicMpBwIDAQABoyowKDAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0l
AQH/BAwwCgYIKwYBBQUHAwMwDQYJKoZIhvcNAQELBQADggEBAED1gdMF0yr8Gy87
RgyaeVpPySwSsO0selmXXrcmOWgiPA05lubyhFEa4P5kdzBEByG2MT+pJkjGYpvK
XRnqXM5VvdS2RhYYFH0cFOIUqBKwCnzViCMuGQeoGUx4oPcKFS0PQ1WjW2d4pS75
51GBfB6LOepsCHUG0A9XEk7EAyUWc4M2ITCJsTtJh8CVn2pTks2q14ETDs86YQv4
peDaJv8nhIe8oQkeGn2o/P/ctkwJg/uBydQUsWgjjGTQZTilVjGTW1mwDr9FucAE
d5gKIk4rtR/3Zd/NDdqp8PrkoWeVM7Hwr789/mpUOeqa/j7YNkDYQh7x+M/odd1D
KY0bQEQ=
-----END CERTIFICATE-----
+2
View File
@@ -1,3 +1,5 @@
import '#/platform/markBundleStartTime'
import '#/platform/polyfills'
import {registerRootComponent} from 'expo'
import {doPolyfill} from '#/lib/api/api-polyfill'
+2
View File
@@ -88,3 +88,5 @@ jest.mock('sentry-expo', () => ({
ReactNavigationInstrumentation: jest.fn(),
},
}))
jest.mock('crypto', () => ({}))
+33 -3
View File
@@ -72,6 +72,7 @@ export async function createServer(
const phoneParams = phoneRequired
? {
phoneVerificationRequired: true,
phoneVerificationProvider: 'twilio',
twilioAccountSid: 'ACXXXXXXX',
twilioAuthToken: 'AUTH',
twilioServiceSid: 'VAXXXXXXXX',
@@ -95,6 +96,35 @@ export async function createServer(
})
mockTwilio(testNet.pds)
// add the test mod authority
if (!phoneRequired) {
const agent = new BskyAgent({service: pdsUrl})
const res = await agent.api.com.atproto.server.createAccount({
email: 'mod-authority@test.com',
handle: 'mod-authority.test',
password: 'hunter2',
})
agent.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`)
await agent.api.app.bsky.actor.profile.create(
{repo: res.data.did},
{
displayName: 'Dev-env Moderation',
description: `The pretend version of mod.bsky.app`,
},
)
await agent.api.app.bsky.labeler.service.create(
{repo: res.data.did, rkey: 'self'},
{
policies: {
labelValues: ['!hide', '!warn'],
labelValueDefinitions: [],
},
createdAt: new Date().toISOString(),
},
)
}
const pic = fs.readFileSync(
path.join(__dirname, '..', 'assets', 'default-avatar.png'),
)
@@ -455,13 +485,13 @@ async function getPort(start = 3000) {
}
export const mockTwilio = (pds: TestPds) => {
if (!pds.ctx.twilio) return
if (!pds.ctx.phoneVerifier) return
pds.ctx.twilio.sendCode = async (_number: string) => {
pds.ctx.phoneVerifier.sendCode = async (_number: string) => {
// do nothing
}
pds.ctx.twilio.verifyCode = async (_number: string, code: string) => {
pds.ctx.phoneVerifier.verifyCode = async (_number: string, code: string) => {
return code === '000000'
}
}
+1
View File
@@ -4,6 +4,7 @@ module.exports = {
'en',
'de',
'es',
'fi',
'fr',
'hi',
'id',
+15 -5
View File
@@ -1,10 +1,11 @@
{
"name": "bsky.app",
"version": "1.71.0",
"version": "1.74.0",
"private": true,
"engines": {
"node": ">=18"
},
"packageManager": "yarn@1.22.19",
"scripts": {
"prepare": "is-ci || husky install",
"postinstall": "patch-package && yarn intl:compile",
@@ -41,12 +42,15 @@
"intl:extract": "lingui extract",
"intl:compile": "lingui compile",
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
"update-extensions": "scripts/updateExtensions.sh"
"update-extensions": "bash scripts/updateExtensions.sh",
"export": "npx expo export",
"make-deploy-bundle": "bash scripts/bundleUpdate.sh"
},
"dependencies": {
"@atproto/api": "^0.10.4",
"@atproto/api": "^0.12.2",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "https://github.com/bluesky-social/react-native-bottom-sheet.git#discord-fork-4.6.1",
"@emoji-mart/react": "^1.1.1",
"@expo/html-elements": "^0.4.2",
"@expo/webpack-config": "^19.0.0",
@@ -54,15 +58,14 @@
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-native-fontawesome": "^0.3.0",
"@gorhom/bottom-sheet": "^4.5.1",
"@lingui/react": "^4.5.0",
"@mattermost/react-native-paste-input": "^0.6.4",
"@miblanchard/react-native-slider": "^2.3.1",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@react-native-async-storage/async-storage": "1.21.0",
"@react-native-camera-roll/camera-roll": "^5.2.2",
"@react-native-clipboard/clipboard": "^1.10.0",
"@react-native-community/blur": "^4.3.0",
"@react-native-community/datetimepicker": "7.6.1",
"@react-native-masked-view/masked-view": "0.3.0",
"@react-native-menu/menu": "^0.8.0",
"@react-native-picker/picker": "2.6.1",
@@ -76,7 +79,9 @@
"@segment/sovran-react-native": "^0.4.5",
"@sentry/react-native": "5.5.0",
"@tamagui/focus-scope": "^1.84.1",
"@tanstack/query-async-storage-persister": "^5.25.0",
"@tanstack/react-query": "^5.8.1",
"@tanstack/react-query-persist-client": "^5.25.0",
"@tiptap/core": "^2.0.0-beta.220",
"@tiptap/extension-document": "^2.0.0-beta.220",
"@tiptap/extension-hard-break": "^2.0.3",
@@ -89,6 +94,7 @@
"@tiptap/pm": "^2.0.0-beta.220",
"@tiptap/react": "^2.0.0-beta.220",
"@tiptap/suggestion": "^2.0.0-beta.220",
"@types/invariant": "^2.2.37",
"@types/node": "^18.16.2",
"@zxing/text-encoding": "^0.9.0",
"array.prototype.findlast": "^1.2.3",
@@ -148,8 +154,10 @@
"react-avatar-editor": "^13.0.0",
"react-circular-progressbar": "^2.1.0",
"react-dom": "^18.2.0",
"react-keyed-flatten-children": "^3.0.0",
"react-native": "0.73.2",
"react-native-appstate-hook": "^1.0.6",
"react-native-date-picker": "^4.4.0",
"react-native-drawer-layout": "^4.0.0-alpha.3",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "~2.14.0",
@@ -177,6 +185,7 @@
"react-responsive": "^9.0.2",
"rn-fetch-blob": "^0.12.0",
"sentry-expo": "~7.0.1",
"statsig-react-native-expo": "^4.6.1",
"tippy.js": "^6.3.7",
"tlds": "^1.234.0",
"use-deep-compare": "^1.1.0",
@@ -230,6 +239,7 @@
"eslint-plugin-lingui": "^0.2.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-native-a11y": "^3.3.0",
"eslint-plugin-simple-import-sort": "^12.0.0",
"html-webpack-plugin": "^5.5.0",
"husky": "^8.0.3",
"is-ci": "^3.0.1",
+55 -7
View File
@@ -1,8 +1,56 @@
diff --git a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt
index 3f50f8c..ee47fa1 100644
--- a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt
+++ b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/ImagePickerModule.kt
@@ -33,7 +33,9 @@ import kotlin.coroutines.resumeWithException
// TODO(@bbarthec): rename to ExpoImagePicker
private const val moduleName = "ExponentImagePicker"
+
class ImagePickerModule : Module() {
+ private var isPickerOpen = false
override fun definition() = ModuleDefinition {
Name(moduleName)
@@ -129,6 +131,11 @@ class ImagePickerModule : Module() {
options: ImagePickerOptions
): Any {
return try {
+ if(isPickerOpen) {
+ return ImagePickerResponse(canceled = true)
+ }
+
+ isPickerOpen = true
var result = launchPicker(pickerLauncher)
if (
!options.allowsMultipleSelection &&
@@ -143,6 +150,8 @@ class ImagePickerModule : Module() {
mediaHandler.readExtras(result.data, options)
} catch (cause: OperationCanceledException) {
return ImagePickerResponse(canceled = true)
+ } finally {
+ isPickerOpen = false
}
}
diff --git a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
index ff15c91..41aaf12 100644
index ff15c91..9763012 100644
--- a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
+++ b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/contracts/ImageLibraryContract.kt
@@ -26,51 +26,26 @@ import java.io.Serializable
@@ -5,12 +5,7 @@ import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.net.Uri
-import androidx.activity.result.PickVisualMediaRequest
-import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
-import androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia
import expo.modules.imagepicker.ImagePickerOptions
-import expo.modules.imagepicker.MediaTypes
-import expo.modules.imagepicker.UNLIMITED_SELECTION
import expo.modules.imagepicker.getAllDataUris
import expo.modules.imagepicker.toMediaType
import expo.modules.kotlin.activityresult.AppContextActivityResultContract
@@ -26,51 +21,26 @@ import java.io.Serializable
* @see [androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents]
*/
internal class ImageLibraryContract(
@@ -12,7 +60,7 @@ index ff15c91..41aaf12 100644
private val contentResolver: ContentResolver
get() = appContextProvider.appContext.reactContext?.contentResolver
?: throw Exceptions.ReactContextLost()
override fun createIntent(context: Context, input: ImageLibraryContractOptions): Intent {
- val request = PickVisualMediaRequest.Builder()
- .setMediaType(
@@ -34,7 +82,7 @@ index ff15c91..41aaf12 100644
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
+ .addCategory(Intent.CATEGORY_OPENABLE)
+ .setType("image/*")
if (input.options.allowsMultipleSelection) {
- val selectionLimit = input.options.selectionLimit
-
@@ -45,7 +93,7 @@ index ff15c91..41aaf12 100644
+ if(input.options.selectionLimit == 1) {
+ return intent
}
- if (selectionLimit > 1) {
- return PickMultipleVisualMedia(selectionLimit).createIntent(context, request)
- }
@@ -56,9 +104,9 @@ index ff15c91..41aaf12 100644
- }
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}
- return PickVisualMedia().createIntent(context, request)
+ return intent
}
override fun parseResult(input: ImageLibraryContractOptions, resultCode: Int, intent: Intent?) =
+15 -88
View File
@@ -1,92 +1,19 @@
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
index 9dca6a5..090bda5 100644
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
@@ -266,11 +266,10 @@ - (void)textViewDidChange:(__unused UITextView *)textView
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
index b09e653..d290dab 100644
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
@@ -198,6 +198,14 @@ - (void)refreshControlValueChanged
[self setCurrentRefreshingState:super.refreshing];
_refreshingProgrammatically = NO;
- (void)textViewDidChangeSelection:(__unused UITextView *)textView
{
- if (_lastStringStateWasUpdatedWith && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
+ if (![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
[self textViewDidChange:_backedTextInputView];
_ignoreNextTextInputCall = YES;
}
- _lastStringStateWasUpdatedWith = _backedTextInputView.attributedText;
[self textViewProbablyDidChangeSelection];
}
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm
index 1f06b79..ab458f3 100644
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm
+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.mm
@@ -87,7 +87,7 @@ - (void)invalidateContentSize
return;
}
- CGSize maximumSize = self.layoutMetrics.frame.size;
+ CGSize maximumSize = self.layoutMetrics.contentFrame.size;
if (_maximumNumberOfLines == 1) {
maximumSize.width = CGFLOAT_MAX;
@@ -158,6 +158,8 @@ - (void)uiManagerWillPerformMounting
[attributedText insertAttributedString:propertyAttributedText atIndex:0];
}
+ [self postprocessAttributedText:attributedText];
+
NSAttributedString *newAttributedText;
if (![_previousAttributedText isEqualToAttributedString:attributedText]) {
// We have to follow `set prop` pattern:
@@ -191,6 +193,52 @@ - (void)uiManagerWillPerformMounting
}];
}
+- (void)postprocessAttributedText:(NSMutableAttributedString *)attributedText
+{
+ __block CGFloat maximumLineHeight = 0;
+
+ [attributedText enumerateAttribute:NSParagraphStyleAttributeName
+ inRange:NSMakeRange(0, attributedText.length)
+ options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
+ usingBlock:^(NSParagraphStyle *paragraphStyle, __unused NSRange range, __unused BOOL *stop) {
+ if (!paragraphStyle) {
+ return;
+ if (@available(iOS 17.4, *)) {
+ if (_currentRefreshingState) {
+ UIImpactFeedbackGenerator *feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
+ [feedbackGenerator prepare];
+ [feedbackGenerator impactOccurred];
+ }
+
+ maximumLineHeight = MAX(paragraphStyle.maximumLineHeight, maximumLineHeight);
+ }];
+
+ if (maximumLineHeight == 0) {
+ // `lineHeight` was not specified, nothing to do.
+ return;
+ }
+
+ __block CGFloat maximumFontLineHeight = 0;
+
+ [attributedText enumerateAttribute:NSFontAttributeName
+ inRange:NSMakeRange(0, attributedText.length)
+ options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
+ usingBlock:^(UIFont *font, NSRange range, __unused BOOL *stop) {
+ if (!font) {
+ return;
+ }
+
+ if (maximumFontLineHeight <= font.lineHeight) {
+ maximumFontLineHeight = font.lineHeight;
+ }
+ }];
+
+ if (maximumLineHeight < maximumFontLineHeight) {
+ return;
+ }
+
+ CGFloat baseLineOffset = maximumLineHeight / 2.0 - maximumFontLineHeight / 2.0;
+
+ [attributedText addAttribute:NSBaselineOffsetAttributeName
+ value:@(baseLineOffset)
+ range:NSMakeRange(0, attributedText.length)];
+}
+
#pragma mark -
- (NSAttributedString *)measurableAttributedText
if (_onRefresh) {
_onRefresh(nil);
}
+4 -4
View File
@@ -1,5 +1,5 @@
# TextInput Patch
# RefreshControl Patch
Patching `RCTBaseTextShadowInput.mm` from https://github.com/facebook/react-native/pull/38359. This fixes some text
getting cut off inside the composer. This was merged in December, so we should be able to remove this patch when RN
ships the next release.
Patching `RCTRefreshControl.mm` temporarily to play an impact haptic on refresh when using iOS 17.4 or higher. Since
17.4, there has been a regression somewhere causing haptics to not play on iOS on refresh. Should monitor for an update
in the RN repo: https://github.com/facebook/react-native/issues/43388
@@ -0,0 +1,37 @@
const {withAndroidManifest} = require('expo/config-plugins')
module.exports = function withAndroidManifestFCMIconPlugin(appConfig) {
return withAndroidManifest(appConfig, function (decoratedAppConfig) {
try {
function addOrModifyMetaData(metaData, name, resource) {
const elem = metaData.find(elem => elem.$['android:name'] === name)
if (elem === undefined) {
metaData.push({
$: {
'android:name': name,
'android:resource': resource,
},
})
} else {
elem.$['android:resource'] = resource
}
}
const androidManifest = decoratedAppConfig.modResults.manifest
const metaData = androidManifest.application[0]['meta-data']
addOrModifyMetaData(
metaData,
'com.google.firebase.messaging.default_notification_color',
'@color/notification_icon_color',
)
addOrModifyMetaData(
metaData,
'com.google.firebase.messaging.default_notification_icon',
'@drawable/notification_icon',
)
return decoratedAppConfig
} catch (e) {
console.error(`withAndroidManifestFCMIconPlugin failed`, e)
}
return decoratedAppConfig
})
}
@@ -0,0 +1,28 @@
const {withStringsXml, AndroidConfig} = require('@expo/config-plugins')
module.exports = function withAndroidSplashScreenStatusBarTranslucentPlugin(
appConfig,
) {
return withStringsXml(appConfig, function (decoratedAppConfig) {
try {
decoratedAppConfig.modResults = AndroidConfig.Strings.setStringItem(
[
{
_: 'true',
$: {
name: 'expo_splash_screen_status_bar_translucent',
translatable: 'false',
},
},
],
decoratedAppConfig.modResults,
)
} catch (e) {
console.error(
`withAndroidSplashScreenStatusBarTranslucentPlugin failed`,
e,
)
}
return decoratedAppConfig
})
}
@@ -0,0 +1,20 @@
const {withAndroidStyles, AndroidConfig} = require('@expo/config-plugins')
module.exports = function withAndroidStylesWindowBackgroundPlugin(appConfig) {
return withAndroidStyles(appConfig, function (decoratedAppConfig) {
try {
decoratedAppConfig.modResults = AndroidConfig.Styles.assignStylesValue(
decoratedAppConfig.modResults,
{
add: true,
parent: AndroidConfig.Styles.getAppThemeLightNoActionBarGroup(),
name: 'android:windowBackground',
value: '@drawable/splashscreen',
},
)
} catch (e) {
console.error(`withAndroidStylesWindowBackgroundPlugin failed`, e)
}
return decoratedAppConfig
})
}
+104
View File
@@ -0,0 +1,104 @@
const crypto = require('crypto')
const fs = require('fs')
const fsp = fs.promises
const path = require('path')
const DIST_DIR = './dist'
const BUNDLES_DIR = '/_expo/static/js'
const IOS_BUNDLE_DIR = path.join(DIST_DIR, BUNDLES_DIR, '/ios')
const ANDROID_BUNDLE_DIR = path.join(DIST_DIR, BUNDLES_DIR, '/android')
const METADATA_PATH = path.join(DIST_DIR, '/metadata.json')
const DEST_DIR = './bundleTempDir'
// Weird, don't feel like figuring out _why_ it wants this
const METADATA = require(`../${METADATA_PATH}`)
const IOS_METADATA_ASSETS = METADATA.fileMetadata.ios.assets
const ANDROID_METADATA_ASSETS = METADATA.fileMetadata.android.assets
const getMd5 = async path => {
return new Promise(res => {
const hash = crypto.createHash('md5')
const rStream = fs.createReadStream(path)
rStream.on('data', data => {
hash.update(data)
})
rStream.on('end', () => {
res(hash.digest('hex'))
})
})
}
const moveFiles = async () => {
console.log('Making directory...')
await fsp.mkdir(DEST_DIR)
await fsp.mkdir(path.join(DEST_DIR, '/assets'))
console.log('Getting ios md5...')
const iosCurrPath = path.join(
IOS_BUNDLE_DIR,
(await fsp.readdir(IOS_BUNDLE_DIR))[0],
)
const iosMd5 = await getMd5(iosCurrPath)
const iosNewPath = `bundles/${iosMd5}.bundle`
console.log('Copying ios bundle...')
await fsp.cp(iosCurrPath, path.join(DEST_DIR, iosNewPath))
console.log('Getting android md5...')
const androidCurrPath = path.join(
ANDROID_BUNDLE_DIR,
(await fsp.readdir(ANDROID_BUNDLE_DIR))[0],
)
const androidMd5 = await getMd5(androidCurrPath)
const androidNewPath = `bundles/${androidMd5}.bundle`
console.log('Copying android bundle...')
await fsp.cp(androidCurrPath, path.join(DEST_DIR, androidNewPath))
const iosAssets = []
const androidAssets = []
console.log('Getting ios asset md5s and moving them...')
for (const asset of IOS_METADATA_ASSETS) {
const currPath = path.join(DIST_DIR, asset.path)
const md5 = await getMd5(currPath)
const withExtPath = `assets/${md5}.${asset.ext}`
iosAssets.push(withExtPath)
await fsp.cp(currPath, path.join(DEST_DIR, withExtPath))
}
console.log('Getting android asset md5s and moving them...')
for (const asset of ANDROID_METADATA_ASSETS) {
const currPath = path.join(DIST_DIR, asset.path)
const md5 = await getMd5(currPath)
const withExtPath = `assets/${md5}.${asset.ext}`
androidAssets.push(withExtPath)
await fsp.cp(currPath, path.join(DEST_DIR, withExtPath))
}
const result = {
version: 0,
bundler: 'metro',
fileMetadata: {
ios: {
bundle: iosNewPath,
assets: iosAssets,
},
android: {
bundle: androidNewPath,
assets: androidAssets,
},
},
}
console.log('Writing metadata...')
await fsp.writeFile(
path.join(DEST_DIR, 'metadata.json'),
JSON.stringify(result),
)
console.log('Finished!')
console.log('Metadata:', result)
}
moveFiles()
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
rm -rf bundleTempDir
rm -rf bundle.tar.gz
echo "Creating tarball..."
node scripts/bundleUpdate.js
cd bundleTempDir || exit
BUNDLE_VERSION=$(date +%s)
DEPLOYMENT_URL="https://updates.bsky.app/v1/upload?runtime-version=$RUNTIME_VERSION&bundle-version=$BUNDLE_VERSION"
tar czvf bundle.tar.gz ./*
echo "Deploying to $DEPLOYMENT_URL..."
curl -o - --form "bundle=@./bundle.tar.gz" --user "bsky:$DENIS_API_KEY" --basic "$DEPLOYMENT_URL"
cd ..
rm -rf bundleTempDir
rm -rf bundle.tar.gz
+85 -72
View File
@@ -1,51 +1,52 @@
import 'react-native-url-polyfill/auto'
import 'lib/sentry' // must be near top
import React, {useState, useEffect} from 'react'
import {RootSiblingParent} from 'react-native-root-siblings'
import * as SplashScreen from 'expo-splash-screen'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {QueryClientProvider} from '@tanstack/react-query'
import {
SafeAreaProvider,
initialWindowMetrics,
} from 'react-native-safe-area-context'
import 'view/icons'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import React, {useEffect, useState} from 'react'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {RootSiblingParent} from 'react-native-root-siblings'
import {
initialWindowMetrics,
SafeAreaProvider,
} from 'react-native-safe-area-context'
import * as SplashScreen from 'expo-splash-screen'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {init as initPersistedState} from '#/state/persisted'
import {listenSessionDropped} from './state/events'
import {ThemeProvider} from 'lib/ThemeContext'
import * as persisted from '#/state/persisted'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {useNotificationsListener} from 'lib/notifications/notifications'
import {QueryProvider} from 'lib/react-query'
import {s} from 'lib/styles'
import {Shell} from 'view/shell'
import * as notifications from 'lib/notifications/notifications'
import * as Toast from 'view/com/util/Toast'
import {queryClient} from 'lib/react-query'
import {TestCtrls} from 'view/com/testing/TestCtrls'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as ModalStateProvider} from 'state/modals'
import {ThemeProvider} from 'lib/ThemeContext'
import {Provider as DialogStateProvider} from 'state/dialogs'
import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
import I18nProvider from './locale/i18nProvider'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import {
Provider as SessionProvider,
useSession,
useSessionApi,
} from 'state/session'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted'
import {Splash} from '#/Splash'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
import {TestCtrls} from 'view/com/testing/TestCtrls'
import * as Toast from 'view/com/util/Toast'
import {Shell} from 'view/shell'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {Provider as PortalProvider} from '#/components/Portal'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {Splash} from '#/Splash'
import I18nProvider from './locale/i18nProvider'
import {listenSessionDropped} from './state/events'
SplashScreen.preventAutoHideAsync()
@@ -54,11 +55,11 @@ function InnerApp() {
const {resumeSession} = useSessionApi()
const theme = useColorModeTheme()
const {_} = useLingui()
useIntentHandler()
// init
useEffect(() => {
notifications.init(queryClient)
listenSessionDropped(() => {
Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
})
@@ -74,21 +75,29 @@ function InnerApp() {
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</RootSiblingParent>
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
<QueryProvider currentDid={currentAccount?.did}>
<PushNotificationsListener>
<StatsigProvider>
<LabelDefsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</RootSiblingParent>
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</LabelDefsProvider>
</StatsigProvider>
</PushNotificationsListener>
</QueryProvider>
</React.Fragment>
</Splash>
</Alf>
@@ -96,6 +105,12 @@ function InnerApp() {
)
}
function PushNotificationsListener({children}: {children: React.ReactNode}) {
const queryClient = useQueryClient()
useNotificationsListener(queryClient)
return children
}
function App() {
const [isReady, setReady] = useState(false)
@@ -112,29 +127,27 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<QueryClientProvider client={queryClient}>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
</QueryClientProvider>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
)
}
+63 -59
View File
@@ -1,38 +1,38 @@
import 'lib/sentry' // must be near top
import React, {useState, useEffect} from 'react'
import {QueryClientProvider} from '@tanstack/react-query'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {RootSiblingParent} from 'react-native-root-siblings'
import 'view/icons'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import React, {useEffect, useState} from 'react'
import {RootSiblingParent} from 'react-native-root-siblings'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {init as initPersistedState} from '#/state/persisted'
import {Shell} from 'view/shell/index'
import {ToastContainer} from 'view/com/util/Toast.web'
import * as persisted from '#/state/persisted'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {QueryProvider} from 'lib/react-query'
import {ThemeProvider} from 'lib/ThemeContext'
import {queryClient} from 'lib/react-query'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as DialogStateProvider} from 'state/dialogs'
import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
import I18nProvider from './locale/i18nProvider'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import {
Provider as SessionProvider,
useSession,
useSessionApi,
} from 'state/session'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import {Provider as SelectedFeedProvider} from 'state/shell/selected-feed'
import {ToastContainer} from 'view/com/util/Toast.web'
import {Shell} from 'view/shell/index'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {Provider as PortalProvider} from '#/components/Portal'
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import I18nProvider from './locale/i18nProvider'
function InnerApp() {
const {isInitialLoad, currentAccount} = useSession()
@@ -54,21 +54,27 @@ function InnerApp() {
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
</RootSiblingParent>
<ToastContainer />
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
<QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<LabelDefsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={theme}>
{/* All components should be within this provider */}
<RootSiblingParent>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
</RootSiblingParent>
<ToastContainer />
</ThemeProvider>
</UnreadNotifsProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</LabelDefsProvider>
</StatsigProvider>
</QueryProvider>
</React.Fragment>
</Alf>
)
@@ -90,29 +96,27 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<QueryClientProvider client={queryClient}>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
</QueryClientProvider>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<I18nProvider>
<PortalProvider>
<InnerApp />
</PortalProvider>
</I18nProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
)
}
+104 -73
View File
@@ -1,83 +1,86 @@
import * as React from 'react'
import {
NavigationContainer,
createNavigationContainerRef,
CommonActions,
StackActions,
DefaultTheme,
DarkTheme,
} from '@react-navigation/native'
import {JSX} from 'react/jsx-runtime'
import {i18n, MessageDescriptor} from '@lingui/core'
import {msg} from '@lingui/macro'
import {
BottomTabBarProps,
createBottomTabNavigator,
} from '@react-navigation/bottom-tabs'
import {
HomeTabNavigatorParams,
SearchTabNavigatorParams,
FeedsTabNavigatorParams,
NotificationsTabNavigatorParams,
FlatNavigatorParams,
AllNavigatorParams,
MyProfileTabNavigatorParams,
BottomTabNavigatorParams,
} from 'lib/routes/types'
import {BottomBar} from './view/shell/bottom-bar/BottomBar'
import {buildStateObject} from 'lib/routes/helpers'
import {State, RouteParams} from 'lib/routes/types'
import {isAndroid, isNative} from 'platform/detection'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {router} from './routes'
import {usePalette} from 'lib/hooks/usePalette'
import {bskyTitle} from 'lib/strings/headings'
import {JSX} from 'react/jsx-runtime'
import {timeout} from 'lib/async/timeout'
import {useUnreadNotifications} from './state/queries/notifications/unread'
import {useSession} from './state/session'
import {useModalControls} from './state/modals'
import {
shouldRequestEmailConfirmation,
setEmailConfirmationRequested,
} from './state/shell/reminders'
import {init as initAnalytics} from './lib/analytics/analytics'
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
CommonActions,
createNavigationContainerRef,
DarkTheme,
DefaultTheme,
NavigationContainer,
StackActions,
} from '@react-navigation/native'
import {HomeScreen} from './view/screens/Home'
import {SearchScreen} from './view/screens/Search'
import {FeedsScreen} from './view/screens/Feeds'
import {NotificationsScreen} from './view/screens/Notifications'
import {ListsScreen} from './view/screens/Lists'
import {ModerationScreen} from './view/screens/Moderation'
import {ModerationModlistsScreen} from './view/screens/ModerationModlists'
import {NotFoundScreen} from './view/screens/NotFound'
import {SettingsScreen} from './view/screens/Settings'
import {LanguageSettingsScreen} from './view/screens/LanguageSettings'
import {ProfileScreen} from './view/screens/Profile'
import {ProfileFollowersScreen} from './view/screens/ProfileFollowers'
import {ProfileFollowsScreen} from './view/screens/ProfileFollows'
import {ProfileFeedScreen} from './view/screens/ProfileFeed'
import {ProfileFeedLikedByScreen} from './view/screens/ProfileFeedLikedBy'
import {ProfileListScreen} from './view/screens/ProfileList'
import {PostThreadScreen} from './view/screens/PostThread'
import {PostLikedByScreen} from './view/screens/PostLikedBy'
import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
import {Storybook} from './view/screens/Storybook'
import {LogScreen} from './view/screens/Log'
import {SupportScreen} from './view/screens/Support'
import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy'
import {TermsOfServiceScreen} from './view/screens/TermsOfService'
import {CommunityGuidelinesScreen} from './view/screens/CommunityGuidelines'
import {CopyrightPolicyScreen} from './view/screens/CopyrightPolicy'
import {timeout} from 'lib/async/timeout'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {usePalette} from 'lib/hooks/usePalette'
import {buildStateObject} from 'lib/routes/helpers'
import {
AllNavigatorParams,
BottomTabNavigatorParams,
FeedsTabNavigatorParams,
FlatNavigatorParams,
HomeTabNavigatorParams,
MyProfileTabNavigatorParams,
NotificationsTabNavigatorParams,
SearchTabNavigatorParams,
} from 'lib/routes/types'
import {RouteParams, State} from 'lib/routes/types'
import {bskyTitle} from 'lib/strings/headings'
import {isAndroid, isNative} from 'platform/detection'
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
import {AppPasswords} from 'view/screens/AppPasswords'
import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts'
import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
import {SavedFeeds} from 'view/screens/SavedFeeds'
import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts'
import {PreferencesFollowingFeed} from 'view/screens/PreferencesFollowingFeed'
import {PreferencesThreads} from 'view/screens/PreferencesThreads'
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStackNavigatorWithAuth'
import {msg} from '@lingui/macro'
import {i18n, MessageDescriptor} from '@lingui/core'
import {SavedFeeds} from 'view/screens/SavedFeeds'
import HashtagScreen from '#/screens/Hashtag'
import {ModerationScreen} from '#/screens/Moderation'
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
import {init as initAnalytics} from './lib/analytics/analytics'
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig'
import {router} from './routes'
import {useModalControls} from './state/modals'
import {useUnreadNotifications} from './state/queries/notifications/unread'
import {useSession} from './state/session'
import {
setEmailConfirmationRequested,
shouldRequestEmailConfirmation,
} from './state/shell/reminders'
import {CommunityGuidelinesScreen} from './view/screens/CommunityGuidelines'
import {CopyrightPolicyScreen} from './view/screens/CopyrightPolicy'
import {DebugModScreen} from './view/screens/DebugMod'
import {FeedsScreen} from './view/screens/Feeds'
import {HomeScreen} from './view/screens/Home'
import {LanguageSettingsScreen} from './view/screens/LanguageSettings'
import {ListsScreen} from './view/screens/Lists'
import {LogScreen} from './view/screens/Log'
import {ModerationModlistsScreen} from './view/screens/ModerationModlists'
import {NotFoundScreen} from './view/screens/NotFound'
import {NotificationsScreen} from './view/screens/Notifications'
import {PostLikedByScreen} from './view/screens/PostLikedBy'
import {PostRepostedByScreen} from './view/screens/PostRepostedBy'
import {PostThreadScreen} from './view/screens/PostThread'
import {PrivacyPolicyScreen} from './view/screens/PrivacyPolicy'
import {ProfileScreen} from './view/screens/Profile'
import {ProfileFeedScreen} from './view/screens/ProfileFeed'
import {ProfileFeedLikedByScreen} from './view/screens/ProfileFeedLikedBy'
import {ProfileFollowersScreen} from './view/screens/ProfileFollowers'
import {ProfileFollowsScreen} from './view/screens/ProfileFollows'
import {ProfileListScreen} from './view/screens/ProfileList'
import {SearchScreen} from './view/screens/Search'
import {SettingsScreen} from './view/screens/Settings'
import {Storybook} from './view/screens/Storybook'
import {SupportScreen} from './view/screens/Support'
import {TermsOfServiceScreen} from './view/screens/TermsOfService'
import {BottomBar} from './view/shell/bottom-bar/BottomBar'
import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStackNavigatorWithAuth'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
@@ -197,11 +200,21 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => ProfileFeedLikedByScreen}
options={{title: title(msg`Liked by`)}}
/>
<Stack.Screen
name="ProfileLabelerLikedBy"
getComponent={() => ProfileLabelerLikedByScreen}
options={{title: title(msg`Liked by`)}}
/>
<Stack.Screen
name="Debug"
getComponent={() => Storybook}
options={{title: title(msg`Storybook`), requireAuth: true}}
/>
<Stack.Screen
name="DebugMod"
getComponent={() => DebugModScreen}
options={{title: title(msg`Moderation states`), requireAuth: true}}
/>
<Stack.Screen
name="Log"
getComponent={() => LogScreen}
@@ -541,15 +554,28 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
ref={navigationRef}
linking={LINKING}
theme={theme}
onStateChange={() => {
logEvent('router:navigate', {})
}}
onReady={() => {
attachRouteToLogEvents(getCurrentRouteName)
logModuleInitTime()
onReady()
logEvent('router:navigate', {})
}}>
{children}
</NavigationContainer>
)
}
function getCurrentRouteName() {
if (navigationRef.isReady()) {
return navigationRef.getCurrentRoute()?.name
} else {
return undefined
}
}
/**
* These helpers can be used from outside of the RoutesContainer
* (eg in the state models).
@@ -649,11 +675,16 @@ function logModuleInitTime() {
return
}
didInit = true
const initMs = Math.round(
// @ts-ignore Emitted by Metro in the bundle prelude
performance.now() - global.__BUNDLE_START_TIME__,
)
console.log(`Time to first paint: ${initMs} ms`)
logEvent('init', {
initMs,
})
if (__DEV__) {
// This log is noisy, so keep false committed
const shouldLog = false
@@ -666,11 +697,11 @@ function logModuleInitTime() {
}
export {
navigate,
resetToTab,
reset,
handleLink,
TabsNavigator,
FlatNavigator,
handleLink,
navigate,
reset,
resetToTab,
RoutesContainer,
TabsNavigator,
}
+92 -3
View File
@@ -1,12 +1,14 @@
import {web, native} from '#/alf/util/platform'
import {Platform} from 'react-native'
import * as tokens from '#/alf/tokens'
import {native, web} from '#/alf/util/platform'
export const atoms = {
/*
* Positioning
*/
fixed: {
position: 'fixed',
position: Platform.select({web: 'fixed', native: 'absolute'}) as 'absolute',
},
absolute: {
position: 'absolute',
@@ -49,6 +51,9 @@ export const atoms = {
h_full: {
height: '100%',
},
h_full_vh: web({
height: '100vh',
}),
/*
* Border radius
@@ -111,6 +116,12 @@ export const atoms = {
flex_row: {
flexDirection: 'row',
},
flex_col_reverse: {
flexDirection: 'column-reverse',
},
flex_row_reverse: {
flexDirection: 'row-reverse',
},
flex_wrap: {
flexWrap: 'wrap',
},
@@ -147,6 +158,12 @@ export const atoms = {
align_end: {
alignItems: 'flex-end',
},
align_baseline: {
alignItems: 'baseline',
},
align_stretch: {
alignItems: 'stretch',
},
self_auto: {
alignSelf: 'auto',
},
@@ -236,9 +253,12 @@ export const atoms = {
font_normal: {
fontWeight: tokens.fontWeight.normal,
},
font_bold: {
font_semibold: {
fontWeight: tokens.fontWeight.semibold,
},
font_bold: {
fontWeight: tokens.fontWeight.bold,
},
italic: {
fontStyle: 'italic',
},
@@ -287,6 +307,9 @@ export const atoms = {
/*
* Padding
*/
p_0: {
padding: 0,
},
p_2xs: {
padding: tokens.space._2xs,
},
@@ -317,6 +340,10 @@ export const atoms = {
p_5xl: {
padding: tokens.space._5xl,
},
px_0: {
paddingLeft: 0,
paddingRight: 0,
},
px_2xs: {
paddingLeft: tokens.space._2xs,
paddingRight: tokens.space._2xs,
@@ -357,6 +384,10 @@ export const atoms = {
paddingLeft: tokens.space._5xl,
paddingRight: tokens.space._5xl,
},
py_0: {
paddingTop: 0,
paddingBottom: 0,
},
py_2xs: {
paddingTop: tokens.space._2xs,
paddingBottom: tokens.space._2xs,
@@ -397,6 +428,9 @@ export const atoms = {
paddingTop: tokens.space._5xl,
paddingBottom: tokens.space._5xl,
},
pt_0: {
paddingTop: 0,
},
pt_2xs: {
paddingTop: tokens.space._2xs,
},
@@ -427,6 +461,9 @@ export const atoms = {
pt_5xl: {
paddingTop: tokens.space._5xl,
},
pb_0: {
paddingBottom: 0,
},
pb_2xs: {
paddingBottom: tokens.space._2xs,
},
@@ -457,6 +494,9 @@ export const atoms = {
pb_5xl: {
paddingBottom: tokens.space._5xl,
},
pl_0: {
paddingLeft: 0,
},
pl_2xs: {
paddingLeft: tokens.space._2xs,
},
@@ -487,6 +527,9 @@ export const atoms = {
pl_5xl: {
paddingLeft: tokens.space._5xl,
},
pr_0: {
paddingRight: 0,
},
pr_2xs: {
paddingRight: tokens.space._2xs,
},
@@ -521,6 +564,9 @@ export const atoms = {
/*
* Margin
*/
m_0: {
margin: 0,
},
m_2xs: {
margin: tokens.space._2xs,
},
@@ -551,6 +597,13 @@ export const atoms = {
m_5xl: {
margin: tokens.space._5xl,
},
m_auto: {
margin: 'auto',
},
mx_0: {
marginLeft: 0,
marginRight: 0,
},
mx_2xs: {
marginLeft: tokens.space._2xs,
marginRight: tokens.space._2xs,
@@ -591,6 +644,14 @@ export const atoms = {
marginLeft: tokens.space._5xl,
marginRight: tokens.space._5xl,
},
mx_auto: {
marginLeft: 'auto',
marginRight: 'auto',
},
my_0: {
marginTop: 0,
marginBottom: 0,
},
my_2xs: {
marginTop: tokens.space._2xs,
marginBottom: tokens.space._2xs,
@@ -631,6 +692,13 @@ export const atoms = {
marginTop: tokens.space._5xl,
marginBottom: tokens.space._5xl,
},
my_auto: {
marginTop: 'auto',
marginBottom: 'auto',
},
mt_0: {
marginTop: 0,
},
mt_2xs: {
marginTop: tokens.space._2xs,
},
@@ -661,6 +729,12 @@ export const atoms = {
mt_5xl: {
marginTop: tokens.space._5xl,
},
mt_auto: {
marginTop: 'auto',
},
mb_0: {
marginBottom: 0,
},
mb_2xs: {
marginBottom: tokens.space._2xs,
},
@@ -691,6 +765,12 @@ export const atoms = {
mb_5xl: {
marginBottom: tokens.space._5xl,
},
mb_auto: {
marginBottom: 'auto',
},
ml_0: {
marginLeft: 0,
},
ml_2xs: {
marginLeft: tokens.space._2xs,
},
@@ -721,6 +801,12 @@ export const atoms = {
ml_5xl: {
marginLeft: tokens.space._5xl,
},
ml_auto: {
marginLeft: 'auto',
},
mr_0: {
marginRight: 0,
},
mr_2xs: {
marginRight: tokens.space._2xs,
},
@@ -751,4 +837,7 @@ export const atoms = {
mr_5xl: {
marginRight: tokens.space._5xl,
},
mr_auto: {
marginRight: 'auto',
},
} as const
+4
View File
@@ -16,6 +16,7 @@ type BreakpointName = keyof typeof breakpoints
const breakpoints: {
[key: string]: number
} = {
gtPhone: 500,
gtMobile: 800,
gtTablet: 1300,
}
@@ -26,6 +27,7 @@ function getActiveBreakpoints({width}: {width: number}) {
return {
active: active[active.length - 1],
gtPhone: active.includes('gtPhone'),
gtMobile: active.includes('gtMobile'),
gtTablet: active.includes('gtTablet'),
}
@@ -39,6 +41,7 @@ export const Context = React.createContext<{
theme: themes.Theme
breakpoints: {
active: BreakpointName | undefined
gtPhone: boolean
gtMobile: boolean
gtTablet: boolean
}
@@ -47,6 +50,7 @@ export const Context = React.createContext<{
theme: themes.light,
breakpoints: {
active: undefined,
gtPhone: false,
gtMobile: false,
gtTablet: false,
},
+7 -4
View File
@@ -1,8 +1,8 @@
import {
BLUE_HUE,
RED_HUE,
GREEN_HUE,
generateScale,
GREEN_HUE,
RED_HUE,
} from '#/alf/util/colorGeneration'
export const scale = generateScale(6, 100)
@@ -12,6 +12,9 @@ export const dimScale = generateScale(12, 100)
export const color = {
trueBlack: '#000000',
temp_purple: 'rgb(105 0 255)',
temp_purple_dark: 'rgb(83 0 202)',
gray_0: `hsl(${BLUE_HUE}, 20%, ${scale[14]}%)`,
gray_25: `hsl(${BLUE_HUE}, 20%, ${scale[13]}%)`,
gray_50: `hsl(${BLUE_HUE}, 20%, ${scale[12]}%)`,
@@ -113,8 +116,8 @@ export const borderRadius = {
export const fontWeight = {
normal: '400',
semibold: '600',
bold: '900',
semibold: '500',
bold: '600',
} as const
export const gradients = {
+17 -15
View File
@@ -15,6 +15,7 @@ import LinearGradient from 'react-native-linear-gradient'
import {useTheme, atoms as a, tokens, android, flatten} from '#/alf'
import {Props as SVGIconProps} from '#/components/icons/common'
import {normalizeTextStyles} from '#/components/Typography'
export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient'
export type ButtonColor =
@@ -27,7 +28,7 @@ export type ButtonColor =
| 'gradient_sunset'
| 'gradient_nordic'
| 'gradient_bonfire'
export type ButtonSize = 'tiny' | 'small' | 'large'
export type ButtonSize = 'tiny' | 'small' | 'medium' | 'large'
export type ButtonShape = 'round' | 'square' | 'default'
export type VariantProps = {
/**
@@ -139,7 +140,7 @@ export function Button({
}))
}, [setState])
const {baseStyles, hoverStyles, focusStyles} = React.useMemo(() => {
const {baseStyles, hoverStyles} = React.useMemo(() => {
const baseStyles: ViewStyle[] = []
const hoverStyles: ViewStyle[] = []
const light = t.name === 'light'
@@ -191,14 +192,14 @@ export function Button({
if (variant === 'solid') {
if (!disabled) {
baseStyles.push({
backgroundColor: t.palette.contrast_50,
backgroundColor: t.palette.contrast_25,
})
hoverStyles.push({
backgroundColor: t.palette.contrast_100,
backgroundColor: t.palette.contrast_50,
})
} else {
baseStyles.push({
backgroundColor: t.palette.contrast_200,
backgroundColor: t.palette.contrast_100,
})
}
} else if (variant === 'outline') {
@@ -274,6 +275,8 @@ export function Button({
if (shape === 'default') {
if (size === 'large') {
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md)
} else if (size === 'medium') {
baseStyles.push({paddingVertical: 12}, a.px_2xl, a.rounded_sm, a.gap_md)
} else if (size === 'small') {
baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
} else if (size === 'tiny') {
@@ -306,12 +309,6 @@ export function Button({
return {
baseStyles,
hoverStyles,
focusStyles: [
...hoverStyles,
{
outline: 0,
} as ViewStyle,
],
}
}, [t, variant, color, size, shape, disabled])
@@ -374,10 +371,8 @@ export function Button({
a.flex_row,
a.align_center,
a.justify_center,
a.justify_center,
flattenedBaseStyles,
...(state.hovered || state.pressed ? hoverStyles : []),
...(state.focused ? focusStyles : []),
flatten(style),
]}
onPressIn={onPressIn}
@@ -396,7 +391,7 @@ export function Button({
]}>
<LinearGradient
colors={
state.hovered || state.pressed || state.focused
state.hovered || state.pressed
? gradientHoverColors
: gradientColors
}
@@ -525,7 +520,14 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
const textStyles = useSharedButtonTextStyles()
return (
<Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}>
<Text
{...rest}
style={normalizeTextStyles([
a.font_bold,
a.text_center,
textStyles,
style,
])}>
{children}
</Text>
)
+13 -10
View File
@@ -31,14 +31,17 @@ export function useDialogControl(): DialogOuterProps['control'] {
}
}, [id, activeDialogs])
return {
id,
ref: control,
open: () => {
control.current.open()
},
close: cb => {
control.current.close(cb)
},
}
return React.useMemo<DialogOuterProps['control']>(
() => ({
id,
ref: control,
open: () => {
control.current.open()
},
close: cb => {
control.current.close(cb)
},
}),
[id, control],
)
}
+39 -45
View File
@@ -1,30 +1,31 @@
import React, {useImperativeHandle} from 'react'
import {View, Dimensions, Keyboard, Pressable} from 'react-native'
import {Dimensions, Pressable, View} from 'react-native'
import Animated, {useAnimatedStyle} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import BottomSheet, {
BottomSheetBackdropProps,
BottomSheetScrollView,
BottomSheetScrollViewMethods,
BottomSheetTextInput,
BottomSheetView,
useBottomSheet,
WINDOW_HEIGHT,
} from '@gorhom/bottom-sheet'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import Animated, {useAnimatedStyle} from 'react-native-reanimated'
} from '@discord/bottom-sheet/src'
import {useTheme, atoms as a, flatten} from '#/alf'
import {Portal} from '#/components/Portal'
import {createInput} from '#/components/forms/TextField'
import {logger} from '#/logger'
import {useDialogStateControlContext} from '#/state/dialogs'
import {isNative} from 'platform/detection'
import {atoms as a, flatten, useTheme} from '#/alf'
import {Context} from '#/components/Dialog/context'
import {
DialogOuterProps,
DialogControlProps,
DialogInnerProps,
DialogOuterProps,
} from '#/components/Dialog/types'
import {Context} from '#/components/Dialog/context'
import {createInput} from '#/components/forms/TextField'
import {Portal} from '#/components/Portal'
export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
export * from '#/components/Dialog/types'
// @ts-ignore
export const Input = createInput(BottomSheetTextInput)
@@ -75,6 +76,7 @@ export function Outer({
control,
onClose,
nativeOptions,
testID,
}: React.PropsWithChildren<DialogOuterProps>) {
const t = useTheme()
const sheet = React.useRef<BottomSheet>(null)
@@ -119,27 +121,20 @@ export function Outer({
[open, close],
)
const onChange = React.useCallback(
(index: number) => {
if (index === -1) {
Keyboard.dismiss()
try {
closeCallback.current?.()
} catch (e: any) {
logger.error(`Dialog closeCallback failed`, {
message: e.message,
})
} finally {
closeCallback.current = undefined
}
setDialogIsOpen(control.id, false)
onClose?.()
setOpenIndex(-1)
}
},
[onClose, setOpenIndex, setDialogIsOpen, control.id],
)
const onCloseInner = React.useCallback(() => {
try {
closeCallback.current?.()
} catch (e: any) {
logger.error(`Dialog closeCallback failed`, {
message: e.message,
})
} finally {
closeCallback.current = undefined
}
setDialogIsOpen(control.id, false)
onClose?.()
setOpenIndex(-1)
}, [control.id, onClose, setDialogIsOpen])
const context = React.useMemo(() => ({close}), [close])
@@ -151,7 +146,8 @@ export function Outer({
accessibilityViewIsModal
// Android
importantForAccessibility="yes"
style={[a.absolute, a.inset_0]}>
style={[a.absolute, a.inset_0]}
testID={testID}>
<BottomSheet
enableDynamicSizing={!hasSnapPoints}
enablePanDownToClose
@@ -167,7 +163,7 @@ export function Outer({
backdropComponent={Backdrop}
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
handleStyle={{display: 'none'}}
onChange={onChange}>
onClose={onCloseInner}>
<Context.Provider value={context}>
<View
style={[
@@ -209,12 +205,14 @@ export function Inner({children, style}: DialogInnerProps) {
)
}
export function ScrollableInner({children, style}: DialogInnerProps) {
export const ScrollableInner = React.forwardRef<
BottomSheetScrollViewMethods,
DialogInnerProps
>(function ScrollableInner({children, style}, ref) {
const insets = useSafeAreaInsets()
return (
<BottomSheetScrollView
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
style={[
a.flex_1, // main diff is this
a.p_xl,
@@ -225,24 +223,20 @@ export function ScrollableInner({children, style}: DialogInnerProps) {
borderTopRightRadius: 40,
},
flatten(style),
]}>
]}
contentContainerStyle={isNative ? a.pb_4xl : undefined}
ref={ref}>
{children}
<View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
</BottomSheetScrollView>
)
}
})
export function Handle() {
const t = useTheme()
const onTouchStart = React.useCallback(() => {
Keyboard.dismiss()
}, [])
return (
<View
style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}
onTouchStart={onTouchStart}>
<View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}>
<View
style={[
a.rounded_sm,
+1 -1
View File
@@ -99,7 +99,7 @@ export function Outer({
style={[
web(a.fixed),
a.inset_0,
{opacity: 0.5, backgroundColor: t.palette.black},
{opacity: 0.8, backgroundColor: t.palette.black},
]}
/>
)}
+17 -3
View File
@@ -1,6 +1,10 @@
import React from 'react'
import type {AccessibilityProps} from 'react-native'
import {BottomSheetProps} from '@gorhom/bottom-sheet'
import type {
AccessibilityProps,
GestureResponderEvent,
ScrollViewProps,
} from 'react-native'
import {BottomSheetProps} from '@discord/bottom-sheet/src'
import {ViewStyleProp} from '#/alf'
@@ -10,9 +14,15 @@ type A11yProps = Required<AccessibilityProps>
* Mutated by useImperativeHandle to provide a public API for controlling the
* dialog. The methods here will actually become the handlers defined within
* the `Dialog.Outer` component.
*
* `Partial<GestureResponderEvent>` here allows us to add this directly to the
* `onPress` prop of a button, for example. If this type was not added, we
* would need to create a function to wrap `.open()` with.
*/
export type DialogControlRefProps = {
open: (options?: DialogControlOpenOptions) => void
open: (
options?: DialogControlOpenOptions & Partial<GestureResponderEvent>,
) => void
close: (callback?: () => void) => void
}
@@ -22,6 +32,7 @@ export type DialogControlRefProps = {
export type DialogControlProps = DialogControlRefProps & {
id: string
ref: React.RefObject<DialogControlRefProps>
isOpen?: boolean
}
export type DialogContextProps = {
@@ -45,6 +56,7 @@ export type DialogOuterProps = {
sheet?: Omit<BottomSheetProps, 'children'>
}
webOptions?: {}
testID?: string
}
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
@@ -53,9 +65,11 @@ export type DialogInnerProps =
label?: undefined
accessibilityLabelledBy: A11yProps['aria-labelledby']
accessibilityDescribedBy: string
keyboardDismissMode?: ScrollViewProps['keyboardDismissMode']
}>
| DialogInnerPropsBase<{
label: string
accessibilityLabelledBy?: undefined
accessibilityDescribedBy?: undefined
keyboardDismissMode?: ScrollViewProps['keyboardDismissMode']
}>
+90
View File
@@ -0,0 +1,90 @@
import React from 'react'
import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {View} from 'react-native'
import {Button} from '#/components/Button'
import {useNavigation} from '@react-navigation/core'
import {NavigationProp} from 'lib/routes/types'
import {StackActions} from '@react-navigation/native'
import {router} from '#/routes'
export function Error({
title,
message,
onRetry,
}: {
title?: string
message?: string
onRetry?: () => unknown
}) {
const navigation = useNavigation<NavigationProp>()
const t = useTheme()
const {gtMobile} = useBreakpoints()
const canGoBack = navigation.canGoBack()
const onGoBack = React.useCallback(() => {
if (canGoBack) {
navigation.goBack()
} else {
navigation.navigate('HomeTab')
// Checking the state for routes ensures that web doesn't encounter errors while going back
if (navigation.getState()?.routes) {
navigation.dispatch(StackActions.push(...router.matchPath('/')))
} else {
navigation.navigate('HomeTab')
navigation.dispatch(StackActions.popToTop())
}
}
}, [navigation, canGoBack])
return (
<CenteredView
style={[
a.flex_1,
a.align_center,
!gtMobile ? a.justify_between : a.gap_5xl,
t.atoms.border_contrast_low,
{paddingTop: 175, paddingBottom: 110},
]}
sideBorders>
<View style={[a.w_full, a.align_center, a.gap_lg]}>
<Text style={[a.font_bold, a.text_3xl]}>{title}</Text>
<Text
style={[
a.text_md,
a.text_center,
t.atoms.text_contrast_high,
{lineHeight: 1.4},
gtMobile && {width: 450},
]}>
{message}
</Text>
</View>
<View style={[a.gap_md, gtMobile ? {width: 350} : [a.w_full, a.px_lg]]}>
{onRetry && (
<Button
variant="solid"
color="primary"
label="Click here"
onPress={onRetry}
size="large"
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
Retry
</Button>
)}
<Button
variant="solid"
color={onRetry ? 'secondary' : 'primary'}
label="Click here"
onPress={onGoBack}
size="large"
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
Go Back
</Button>
</View>
</CenteredView>
)
}
+27
View File
@@ -0,0 +1,27 @@
import React from 'react'
import LinearGradient from 'react-native-linear-gradient'
import {atoms as a, tokens} from '#/alf'
export function GradientFill({
gradient,
}: {
gradient:
| typeof tokens.gradients.sky
| typeof tokens.gradients.midnight
| typeof tokens.gradients.sunrise
| typeof tokens.gradients.sunset
| typeof tokens.gradients.bonfire
| typeof tokens.gradients.summer
| typeof tokens.gradients.nordic
}) {
return (
<LinearGradient
colors={gradient.values.map(c => c[1])}
locations={gradient.values.map(c => c[0])}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[a.absolute, a.inset_0]}
/>
)
}
@@ -0,0 +1,182 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppBskyLabelerDefs} from '@atproto/api'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {Link as InternalLink, LinkProps} from '#/components/Link'
import {Text} from '#/components/Typography'
import {useLabelerInfoQuery} from '#/state/queries/labeler'
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
import {RichText} from '#/components/RichText'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '../icons/Chevron'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {sanitizeHandle} from '#/lib/strings/handles'
import {pluralize} from '#/lib/strings/helpers'
type LabelingServiceProps = {
labeler: AppBskyLabelerDefs.LabelerViewDetailed
}
export function Outer({
children,
style,
}: React.PropsWithChildren<ViewStyleProp>) {
return (
<View
style={[
a.flex_row,
a.gap_md,
a.w_full,
a.p_lg,
a.pr_md,
a.overflow_hidden,
style,
]}>
{children}
</View>
)
}
export function Avatar({avatar}: {avatar?: string}) {
return <UserAvatar type="labeler" size={40} avatar={avatar} />
}
export function Title({value}: {value: string}) {
return <Text style={[a.text_md, a.font_bold]}>{value}</Text>
}
export function Description({value, handle}: {value?: string; handle: string}) {
return value ? (
<Text numberOfLines={2}>
<RichText value={value} style={[]} />
</Text>
) : (
<Text>
<Trans>By {sanitizeHandle(handle, '@')}</Trans>
</Text>
)
}
export function LikeCount({count}: {count: number}) {
const t = useTheme()
return (
<Text
style={[
a.mt_sm,
a.text_sm,
t.atoms.text_contrast_medium,
{fontWeight: '500'},
]}>
<Trans>
Liked by {count} {pluralize(count, 'user')}
</Trans>
</Text>
)
}
export function Content({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
return (
<View
style={[
a.flex_1,
a.flex_row,
a.gap_md,
a.align_center,
a.justify_between,
]}>
<View style={[a.gap_xs, a.flex_1]}>{children}</View>
<ChevronRight size="md" style={[a.z_10, t.atoms.text_contrast_low]} />
</View>
)
}
/**
* The canonical view for a labeling service. Use this or compose your own.
*/
export function Default({
labeler,
style,
}: LabelingServiceProps & ViewStyleProp) {
return (
<Outer style={style}>
<Avatar avatar={labeler.creator.avatar} />
<Content>
<Title
value={getLabelingServiceTitle({
displayName: labeler.creator.displayName,
handle: labeler.creator.handle,
})}
/>
<Description
value={labeler.creator.description}
handle={labeler.creator.handle}
/>
{labeler.likeCount ? <LikeCount count={labeler.likeCount} /> : null}
</Content>
</Outer>
)
}
export function Link({
children,
labeler,
}: LabelingServiceProps & Pick<LinkProps, 'children'>) {
const {_} = useLingui()
return (
<InternalLink
to={{
screen: 'Profile',
params: {
name: labeler.creator.handle,
},
}}
label={_(
msg`View the labeling service provided by @${labeler.creator.handle}`,
)}>
{children}
</InternalLink>
)
}
// TODO not finished yet
export function DefaultSkeleton() {
return (
<View>
<Text>Loading</Text>
</View>
)
}
export function Loader({
did,
loading: LoadingComponent = DefaultSkeleton,
error: ErrorComponent,
component: Component,
}: {
did: string
loading?: React.ComponentType<{}>
error?: React.ComponentType<{error: string}>
component: React.ComponentType<{
labeler: AppBskyLabelerDefs.LabelerViewDetailed
}>
}) {
const {isLoading, data, error} = useLabelerInfoQuery({did})
return isLoading ? (
LoadingComponent ? (
<LoadingComponent />
) : null
) : error || !data ? (
ErrorComponent ? (
<ErrorComponent error={error?.message || 'Unknown error'} />
) : null
) : (
<Component labeler={data} />
)
}
+109
View File
@@ -0,0 +1,109 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {logger} from '#/logger'
import {List} from '#/view/com/util/List'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {useLikedByQuery} from '#/state/queries/post-liked-by'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {ListFooter} from '#/components/Lists'
import {atoms as a, useTheme} from '#/alf'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
export function LikedByList({uri}: {uri: string}) {
const t = useTheme()
const [isPTRing, setIsPTRing] = React.useState(false)
const {
data: resolvedUri,
error: resolveError,
isFetching: isFetchingResolvedUri,
} = useResolveUriQuery(uri)
const {
data,
isFetching,
isFetched,
isRefetching,
hasNextPage,
fetchNextPage,
isError,
error: likedByError,
refetch,
} = useLikedByQuery(resolvedUri?.uri)
const likes = React.useMemo(() => {
if (data?.pages) {
return data.pages.flatMap(page => page.likes)
}
return []
}, [data])
const initialNumToRender = useInitialNumToRender()
const error = resolveError || likedByError
const onRefresh = React.useCallback(async () => {
setIsPTRing(true)
try {
await refetch()
} catch (err) {
logger.error('Failed to refresh likes', {message: err})
}
setIsPTRing(false)
}, [refetch, setIsPTRing])
const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more likes', {message: err})
}
}, [isFetching, hasNextPage, isError, fetchNextPage])
const renderItem = React.useCallback(({item}: {item: GetLikes.Like}) => {
return (
<ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
)
}, [])
if (isFetchingResolvedUri || !isFetched) {
return (
<View style={[a.w_full, a.align_center, a.p_lg]}>
<Loader size="xl" />
</View>
)
}
return likes.length ? (
<List
data={likes}
keyExtractor={item => item.actor.did}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={3}
renderItem={renderItem}
initialNumToRender={initialNumToRender}
ListFooterComponent={() => (
<ListFooter
isFetching={isFetching && !isRefetching}
isError={isError}
error={error ? error.toString() : undefined}
onRetry={fetchNextPage}
/>
)}
/>
) : (
<View style={[a.p_lg]}>
<View style={[a.p_lg, a.rounded_sm, t.atoms.bg_contrast_25]}>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
Nobody has liked this yet. Maybe you should be the first!
</Trans>
</Text>
</View>
</View>
)
}
+131
View File
@@ -0,0 +1,131 @@
import React, {useMemo, useCallback} from 'react'
import {ActivityIndicator, FlatList, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {useLikedByQuery} from '#/state/queries/post-liked-by'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import * as Dialog from '#/components/Dialog'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {Loader} from '#/components/Loader'
interface LikesDialogProps {
control: Dialog.DialogOuterProps['control']
uri: string
}
export function LikesDialog(props: LikesDialogProps) {
return (
<Dialog.Outer control={props.control}>
<Dialog.Handle />
<LikesDialogInner {...props} />
</Dialog.Outer>
)
}
export function LikesDialogInner({control, uri}: LikesDialogProps) {
const {_} = useLingui()
const t = useTheme()
const {
data: resolvedUri,
error: resolveError,
isFetched: hasFetchedResolvedUri,
} = useResolveUriQuery(uri)
const {
data,
isFetching: isFetchingLikedBy,
isFetched: hasFetchedLikedBy,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
isError,
error: likedByError,
} = useLikedByQuery(resolvedUri?.uri)
const isLoading = !hasFetchedResolvedUri || !hasFetchedLikedBy
const likes = useMemo(() => {
if (data?.pages) {
return data.pages.flatMap(page => page.likes)
}
return []
}, [data])
const onEndReached = useCallback(async () => {
if (isFetchingLikedBy || !hasNextPage || isError) return
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more likes', {message: err})
}
}, [isFetchingLikedBy, hasNextPage, isError, fetchNextPage])
const renderItem = useCallback(
({item}: {item: GetLikes.Like}) => {
return (
<ProfileCardWithFollowBtn
key={item.actor.did}
profile={item.actor}
onPress={() => control.close()}
/>
)
},
[control],
)
return (
<Dialog.Inner label={_(msg`Users that have liked this content or profile`)}>
<Text style={[a.text_2xl, a.font_bold, a.leading_tight, a.pb_lg]}>
<Trans>Liked by</Trans>
</Text>
{isLoading ? (
<View style={{minHeight: 300}}>
<Loader size="xl" />
</View>
) : resolveError || likedByError || !data ? (
<ErrorMessage message={cleanError(resolveError || likedByError)} />
) : likes.length === 0 ? (
<View style={[t.atoms.bg_contrast_50, a.px_md, a.py_xl, a.rounded_md]}>
<Text style={[a.text_center]}>
<Trans>
Nobody has liked this yet. Maybe you should be the first!
</Trans>
</Text>
</View>
) : (
<FlatList
data={likes}
keyExtractor={item => item.actor.did}
onEndReached={onEndReached}
renderItem={renderItem}
initialNumToRender={15}
ListFooterComponent={
<ListFooterComponent isFetching={isFetchingNextPage} />
}
/>
)}
<Dialog.Close />
</Dialog.Inner>
)
}
function ListFooterComponent({isFetching}: {isFetching: boolean}) {
if (isFetching) {
return (
<View style={a.pt_lg}>
<ActivityIndicator />
</View>
)
}
return null
}
+8 -9
View File
@@ -1,17 +1,13 @@
import React from 'react'
import {GestureResponderEvent} from 'react-native'
import {
useLinkProps,
useNavigation,
StackActions,
} from '@react-navigation/native'
import {useLinkProps, StackActions} from '@react-navigation/native'
import {sanitizeUrl} from '@braintree/sanitize-url'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {isWeb} from '#/platform/detection'
import {useTheme, web, flatten, TextStyleProp, atoms as a} from '#/alf'
import {Button, ButtonProps} from '#/components/Button'
import {AllNavigatorParams, NavigationProp} from '#/lib/routes/types'
import {AllNavigatorParams} from '#/lib/routes/types'
import {
convertBskyAppUrlIfNeeded,
isExternalUrl,
@@ -21,6 +17,7 @@ import {useModalControls} from '#/state/modals'
import {router} from '#/routes'
import {Text, TextProps} from '#/components/Typography'
import {useOpenLink} from 'state/preferences/in-app-browser'
import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
/**
* Only available within a `Link`, since that inherits from `Button`.
@@ -74,7 +71,7 @@ export function useLink({
}: BaseLinkProps & {
displayText: string
}) {
const navigation = useNavigation<NavigationProp>()
const navigation = useNavigationDeduped()
const {href} = useLinkProps<AllNavigatorParams>({
to:
typeof to === 'string' ? convertBskyAppUrlIfNeeded(sanitizeUrl(to)) : to,
@@ -231,6 +228,7 @@ export function InlineLink({
onPress: outerOnPress,
download,
selectable,
label,
...rest
}: InlineLinkProps) {
const t = useTheme()
@@ -253,12 +251,13 @@ export function InlineLink({
onIn: onPressIn,
onOut: onPressOut,
} = useInteractionState()
const flattenedStyle = flatten(style)
const flattenedStyle = flatten(style) || {}
return (
<Text
selectable={selectable}
label={href}
accessibilityHint=""
accessibilityLabel={label || href}
{...rest}
style={[
{color: t.palette.primary_500},
+69 -115
View File
@@ -1,26 +1,28 @@
import React from 'react'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {View} from 'react-native'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {CenteredView} from 'view/com/util/Views'
import {Loader} from '#/components/Loader'
import {Trans} from '@lingui/macro'
import {cleanError} from 'lib/strings/errors'
import {Button} from '#/components/Button'
import {Text} from '#/components/Typography'
import {StackActions} from '@react-navigation/native'
import {useNavigation} from '@react-navigation/core'
import {NavigationProp} from 'lib/routes/types'
import {router} from '#/routes'
import {Error} from '#/components/Error'
export function ListFooter({
isFetching,
isError,
error,
onRetry,
height,
}: {
isFetching: boolean
isError: boolean
isFetching?: boolean
isError?: boolean
error?: string
onRetry?: () => Promise<unknown>
height?: number
}) {
const t = useTheme()
@@ -29,11 +31,10 @@ export function ListFooter({
style={[
a.w_full,
a.align_center,
a.justify_center,
a.border_t,
a.pb_lg,
t.atoms.border_contrast_low,
{height: 100},
{height: height ?? 180, paddingTop: 30},
]}>
{isFetching ? (
<Loader size="xl" />
@@ -53,11 +54,12 @@ function ListFooterMaybeError({
error,
onRetry,
}: {
isError: boolean
isError?: boolean
error?: string
onRetry?: () => Promise<unknown>
}) {
const t = useTheme()
const {_} = useLingui()
if (!isError) return null
@@ -83,7 +85,7 @@ function ListFooterMaybeError({
</Text>
<Button
variant="gradient"
label="Press to retry"
label={_(msg`Press to retry`)}
style={[
a.align_center,
a.justify_center,
@@ -93,7 +95,7 @@ function ListFooterMaybeError({
a.py_sm,
]}
onPress={onRetry}>
Retry
<Trans>Retry</Trans>
</Button>
</View>
</View>
@@ -128,119 +130,71 @@ export function ListMaybePlaceholder({
isLoading,
isEmpty,
isError,
empty,
error,
notFoundType = 'page',
emptyTitle,
emptyMessage,
errorTitle,
errorMessage,
emptyType = 'page',
onRetry,
}: {
isLoading: boolean
isEmpty: boolean
isError: boolean
empty?: string
error?: string
notFoundType?: 'page' | 'results'
isEmpty?: boolean
isError?: boolean
emptyTitle?: string
emptyMessage?: string
errorTitle?: string
errorMessage?: string
emptyType?: 'page' | 'results'
onRetry?: () => Promise<unknown>
}) {
const navigation = useNavigation<NavigationProp>()
const t = useTheme()
const {gtMobile} = useBreakpoints()
const {_} = useLingui()
const {gtMobile, gtTablet} = useBreakpoints()
const canGoBack = navigation.canGoBack()
const onGoBack = React.useCallback(() => {
if (canGoBack) {
navigation.goBack()
} else {
navigation.navigate('HomeTab')
if (!isLoading && isError) {
return (
<Error
title={errorTitle ?? _(msg`Oops!`)}
message={errorMessage ?? _(`Something went wrong!`)}
onRetry={onRetry}
/>
)
}
// Checking the state for routes ensures that web doesn't encounter errors while going back
if (navigation.getState()?.routes) {
navigation.dispatch(StackActions.push(...router.matchPath('/')))
} else {
navigation.navigate('HomeTab')
navigation.dispatch(StackActions.popToTop())
}
}
}, [navigation, canGoBack])
if (!isEmpty) return null
return (
<View
style={[
a.flex_1,
a.align_center,
!gtMobile ? [a.justify_between, a.border_t] : a.gap_5xl,
t.atoms.border_contrast_low,
{paddingTop: 175, paddingBottom: 110},
]}>
{isLoading ? (
if (isLoading) {
return (
<CenteredView
style={[
a.flex_1,
a.align_center,
!gtMobile ? a.justify_between : a.gap_5xl,
t.atoms.border_contrast_low,
{paddingTop: 175, paddingBottom: 110},
]}
sideBorders={gtMobile}
topBorder={!gtTablet}>
<View style={[a.w_full, a.align_center, {top: 100}]}>
<Loader size="xl" />
</View>
) : (
<>
<View style={[a.w_full, a.align_center, a.gap_lg]}>
<Text style={[a.font_bold, a.text_3xl]}>
{isError ? (
<Trans>Oops!</Trans>
) : isEmpty ? (
<>
{notFoundType === 'results' ? (
<Trans>No results found</Trans>
) : (
<Trans>Page not found</Trans>
)}
</>
) : undefined}
</Text>
</CenteredView>
)
}
{isError ? (
<Text
style={[a.text_md, a.text_center, t.atoms.text_contrast_high]}>
{error ? error : <Trans>Something went wrong!</Trans>}
</Text>
) : isEmpty ? (
<Text
style={[a.text_md, a.text_center, t.atoms.text_contrast_high]}>
{empty ? (
empty
) : (
<Trans>
We're sorry! We can't find the page you were looking for.
</Trans>
)}
</Text>
) : undefined}
</View>
<View
style={[a.gap_md, !gtMobile ? [a.w_full, a.px_lg] : {width: 350}]}>
{isError && onRetry && (
<Button
variant="solid"
color="primary"
label="Click here"
onPress={onRetry}
size="large"
style={[
a.rounded_sm,
a.overflow_hidden,
{paddingVertical: 10},
]}>
Retry
</Button>
)}
<Button
variant="solid"
color={isError && onRetry ? 'secondary' : 'primary'}
label="Click here"
onPress={onGoBack}
size="large"
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
Go Back
</Button>
</View>
</>
)}
</View>
)
if (isEmpty) {
return (
<Error
title={
emptyTitle ??
(emptyType === 'results'
? _(msg`No results found`)
: _(msg`Page not found`))
}
message={
emptyMessage ??
_(msg`We're sorry! We can't find the page you were looking for.`)
}
onRetry={onRetry}
/>
)
}
}
+8
View File
@@ -0,0 +1,8 @@
import React from 'react'
import type {ContextType} from '#/components/Menu/types'
export const Context = React.createContext<ContextType>({
// @ts-ignore
control: null,
})
+221
View File
@@ -0,0 +1,221 @@
import React from 'react'
import {View, Pressable, ViewStyle, StyleProp} from 'react-native'
import flattenReactChildren from 'react-keyed-flatten-children'
import {atoms as a, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Text} from '#/components/Typography'
import {Context} from '#/components/Menu/context'
import {
ContextType,
TriggerProps,
ItemProps,
GroupProps,
ItemTextProps,
ItemIconProps,
} from '#/components/Menu/types'
import {Button, ButtonText} from '#/components/Button'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isNative} from 'platform/detection'
export {useDialogControl as useMenuControl} from '#/components/Dialog'
export function useMemoControlContext() {
return React.useContext(Context)
}
export function Root({
children,
control,
}: React.PropsWithChildren<{
control?: Dialog.DialogOuterProps['control']
}>) {
const defaultControl = Dialog.useDialogControl()
const context = React.useMemo<ContextType>(
() => ({
control: control || defaultControl,
}),
[control, defaultControl],
)
return <Context.Provider value={context}>{children}</Context.Provider>
}
export function Trigger({children, label}: TriggerProps) {
const {control} = React.useContext(Context)
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const {
state: pressed,
onIn: onPressIn,
onOut: onPressOut,
} = useInteractionState()
return children({
isNative: true,
control,
state: {
hovered: false,
focused,
pressed,
},
props: {
onPress: control.open,
onFocus,
onBlur,
onPressIn,
onPressOut,
accessibilityLabel: label,
},
})
}
export function Outer({
children,
showCancel,
}: React.PropsWithChildren<{
showCancel?: boolean
style?: StyleProp<ViewStyle>
}>) {
const context = React.useContext(Context)
return (
<Dialog.Outer control={context.control}>
<Dialog.Handle />
{/* Re-wrap with context since Dialogs are portal-ed to root */}
<Context.Provider value={context}>
<Dialog.ScrollableInner label="Menu TODO">
<View style={[a.gap_lg]}>
{children}
{isNative && showCancel && <Cancel />}
</View>
<View style={{height: a.gap_lg.gap}} />
</Dialog.ScrollableInner>
</Context.Provider>
</Dialog.Outer>
)
}
export function Item({children, label, style, onPress, ...rest}: ItemProps) {
const t = useTheme()
const {control} = React.useContext(Context)
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const {
state: pressed,
onIn: onPressIn,
onOut: onPressOut,
} = useInteractionState()
return (
<Pressable
{...rest}
accessibilityHint=""
accessibilityLabel={label}
onPress={e => {
onPress(e)
if (!e.defaultPrevented) {
control?.close()
}
}}
onFocus={onFocus}
onBlur={onBlur}
onPressIn={onPressIn}
onPressOut={onPressOut}
style={[
a.flex_row,
a.align_center,
a.gap_sm,
a.px_md,
a.rounded_md,
a.border,
t.atoms.bg_contrast_25,
t.atoms.border_contrast_low,
{minHeight: 44, paddingVertical: 10},
style,
(focused || pressed) && [t.atoms.bg_contrast_50],
]}>
{children}
</Pressable>
)
}
export function ItemText({children, style}: ItemTextProps) {
const t = useTheme()
return (
<Text
numberOfLines={1}
ellipsizeMode="middle"
style={[
a.flex_1,
a.text_md,
a.font_bold,
t.atoms.text_contrast_medium,
{paddingTop: 3},
style,
]}>
{children}
</Text>
)
}
export function ItemIcon({icon: Comp}: ItemIconProps) {
const t = useTheme()
return <Comp size="lg" fill={t.atoms.text_contrast_medium.color} />
}
export function Group({children, style}: GroupProps) {
const t = useTheme()
return (
<View
style={[
a.rounded_md,
a.overflow_hidden,
a.border,
t.atoms.border_contrast_low,
style,
]}>
{flattenReactChildren(children).map((child, i) => {
return React.isValidElement(child) && child.type === Item ? (
<React.Fragment key={i}>
{i > 0 ? (
<View style={[a.border_b, t.atoms.border_contrast_low]} />
) : null}
{React.cloneElement(child, {
// @ts-ignore
style: {
borderRadius: 0,
borderWidth: 0,
},
})}
</React.Fragment>
) : null
})}
</View>
)
}
function Cancel() {
const {_} = useLingui()
const {control} = React.useContext(Context)
return (
<Button
label={_(msg`Close this dialog`)}
size="small"
variant="ghost"
color="secondary"
onPress={() => control.close()}>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
)
}
export function Divider() {
return null
}
+293
View File
@@ -0,0 +1,293 @@
/* eslint-disable react/prop-types */
import React from 'react'
import {View, Pressable, ViewStyle, StyleProp} from 'react-native'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as Dialog from '#/components/Dialog'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {atoms as a, useTheme, flatten, web} from '#/alf'
import {Text} from '#/components/Typography'
import {
ContextType,
TriggerProps,
ItemProps,
GroupProps,
ItemTextProps,
ItemIconProps,
RadixPassThroughTriggerProps,
} from '#/components/Menu/types'
import {Context} from '#/components/Menu/context'
import {Portal} from '#/components/Portal'
export function useMenuControl(): Dialog.DialogControlProps {
const id = React.useId()
const [isOpen, setIsOpen] = React.useState(false)
return React.useMemo(
() => ({
id,
ref: {current: null},
isOpen,
open() {
setIsOpen(true)
},
close() {
setIsOpen(false)
},
}),
[id, isOpen, setIsOpen],
)
}
export function useMemoControlContext() {
return React.useContext(Context)
}
export function Root({
children,
control,
}: React.PropsWithChildren<{
control?: Dialog.DialogOuterProps['control']
}>) {
const {_} = useLingui()
const defaultControl = useMenuControl()
const context = React.useMemo<ContextType>(
() => ({
control: control || defaultControl,
}),
[control, defaultControl],
)
const onOpenChange = React.useCallback(
(open: boolean) => {
if (context.control.isOpen && !open) {
context.control.close()
} else if (!context.control.isOpen && open) {
context.control.open()
}
},
[context.control],
)
return (
<Context.Provider value={context}>
{context.control.isOpen && (
<Portal>
<Pressable
style={[a.fixed, a.inset_0, a.z_50]}
onPress={() => context.control.close()}
accessibilityHint=""
accessibilityLabel={_(
msg`Context menu backdrop, click to close the menu.`,
)}
/>
</Portal>
)}
<DropdownMenu.Root
open={context.control.isOpen}
onOpenChange={onOpenChange}>
{children}
</DropdownMenu.Root>
</Context.Provider>
)
}
const RadixTriggerPassThrough = React.forwardRef(
(
props: {
children: (
props: RadixPassThroughTriggerProps & {
ref: React.Ref<any>
},
) => React.ReactNode
},
ref,
) => {
// @ts-expect-error Radix provides no types of this stuff
return props.children({...props, ref})
},
)
RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
export function Trigger({children, label}: TriggerProps) {
const {control} = React.useContext(Context)
const {
state: hovered,
onIn: onMouseEnter,
onOut: onMouseLeave,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
return (
<DropdownMenu.Trigger asChild>
<RadixTriggerPassThrough>
{props =>
children({
isNative: false,
control,
state: {
hovered,
focused,
pressed: false,
},
props: {
...props,
// disable on web, use `onPress`
onPointerDown: () => false,
onPress: () =>
control.isOpen ? control.close() : control.open(),
onFocus: onFocus,
onBlur: onBlur,
onMouseEnter,
onMouseLeave,
accessibilityLabel: label,
},
})
}
</RadixTriggerPassThrough>
</DropdownMenu.Trigger>
)
}
export function Outer({
children,
style,
}: React.PropsWithChildren<{
showCancel?: boolean
style?: StyleProp<ViewStyle>
}>) {
const t = useTheme()
return (
<DropdownMenu.Portal>
<DropdownMenu.Content sideOffset={5} loop aria-label="Test">
<View
style={[
a.rounded_sm,
a.p_xs,
t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25,
t.atoms.shadow_md,
style,
]}>
{children}
</View>
{/* Disabled until we can fix positioning
<DropdownMenu.Arrow
className="DropdownMenuArrow"
fill={
(t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25)
.backgroundColor
}
/>
*/}
</DropdownMenu.Content>
</DropdownMenu.Portal>
)
}
export function Item({children, label, onPress, ...rest}: ItemProps) {
const t = useTheme()
const {control} = React.useContext(Context)
const {
state: hovered,
onIn: onMouseEnter,
onOut: onMouseLeave,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
return (
<DropdownMenu.Item asChild>
<Pressable
{...rest}
className="radix-dropdown-item"
accessibilityHint=""
accessibilityLabel={label}
onPress={e => {
onPress(e)
/**
* Ported forward from Radix
* @see https://www.radix-ui.com/primitives/docs/components/dropdown-menu#item
*/
if (!e.defaultPrevented) {
control.close()
}
}}
onFocus={onFocus}
onBlur={onBlur}
// need `flatten` here for Radix compat
style={flatten([
a.flex_row,
a.align_center,
a.gap_lg,
a.py_sm,
a.rounded_xs,
{minHeight: 32, paddingHorizontal: 10},
web({outline: 0}),
(hovered || focused) && [
web({outline: '0 !important'}),
t.name === 'light'
? t.atoms.bg_contrast_25
: t.atoms.bg_contrast_50,
],
])}
{...web({
onMouseEnter,
onMouseLeave,
})}>
{children}
</Pressable>
</DropdownMenu.Item>
)
}
export function ItemText({children, style}: ItemTextProps) {
const t = useTheme()
return (
<Text style={[a.flex_1, a.font_bold, t.atoms.text_contrast_high, style]}>
{children}
</Text>
)
}
export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) {
const t = useTheme()
return (
<Comp
size="md"
fill={t.atoms.text_contrast_medium.color}
style={[
position === 'left' && {
marginLeft: -2,
},
position === 'right' && {
marginRight: -2,
marginLeft: 12,
},
]}
/>
)
}
export function Group({children}: GroupProps) {
return children
}
export function Divider() {
const t = useTheme()
return (
<DropdownMenu.Separator
style={flatten([
a.my_xs,
t.atoms.bg_contrast_100,
{
height: 1,
},
])}
/>
)
}
+99
View File
@@ -0,0 +1,99 @@
import React from 'react'
import {
GestureResponderEvent,
PressableProps,
AccessibilityProps,
} from 'react-native'
import {Props as SVGIconProps} from '#/components/icons/common'
import * as Dialog from '#/components/Dialog'
import {TextStyleProp, ViewStyleProp} from '#/alf'
export type ContextType = {
control: Dialog.DialogOuterProps['control']
}
export type RadixPassThroughTriggerProps = {
id: string
type: 'button'
disabled: boolean
['data-disabled']: boolean
['data-state']: string
['aria-controls']?: string
['aria-haspopup']?: boolean
['aria-expanded']?: AccessibilityProps['aria-expanded']
onKeyDown: (e: React.KeyboardEvent) => void
/**
* Radix provides this, but we override on web to use `onPress` instead,
* which is less sensitive while scrolling.
*/
onPointerDown: PressableProps['onPointerDown']
}
export type TriggerProps = {
children(props: TriggerChildProps): React.ReactNode
label: string
}
export type TriggerChildProps =
| {
isNative: true
control: Dialog.DialogOuterProps['control']
state: {
/**
* Web only, `false` on native
*/
hovered: false
focused: boolean
pressed: boolean
}
/**
* We don't necessarily know what these will be spread on to, so we
* should add props one-by-one.
*
* On web, these properties are applied to a parent `Pressable`, so this
* object is empty.
*/
props: {
onPress: () => void
onFocus: () => void
onBlur: () => void
onPressIn: () => void
onPressOut: () => void
accessibilityLabel: string
}
}
| {
isNative: false
control: Dialog.DialogOuterProps['control']
state: {
hovered: boolean
focused: boolean
/**
* Native only, `false` on web
*/
pressed: false
}
props: RadixPassThroughTriggerProps & {
onPress: () => void
onFocus: () => void
onBlur: () => void
onMouseEnter: () => void
onMouseLeave: () => void
accessibilityLabel: string
}
}
export type ItemProps = React.PropsWithChildren<
Omit<PressableProps, 'style'> &
ViewStyleProp & {
label: string
onPress: (e: GestureResponderEvent) => void
}
>
export type ItemTextProps = React.PropsWithChildren<TextStyleProp & {}>
export type ItemIconProps = React.PropsWithChildren<{
icon: React.ComponentType<SVGIconProps>
position?: 'left' | 'right'
}>
export type GroupProps = React.PropsWithChildren<ViewStyleProp & {}>
+94 -20
View File
@@ -1,11 +1,11 @@
import React from 'react'
import {View, PressableProps} from 'react-native'
import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useTheme, atoms as a} from '#/alf'
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import {Button} from '#/components/Button'
import {Button, ButtonColor, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
@@ -22,9 +22,12 @@ const Context = React.createContext<{
export function Outer({
children,
control,
testID,
}: React.PropsWithChildren<{
control: Dialog.DialogOuterProps['control']
testID?: string
}>) {
const {gtMobile} = useBreakpoints()
const titleId = React.useId()
const descriptionId = React.useId()
@@ -34,16 +37,16 @@ export function Outer({
)
return (
<Dialog.Outer control={control}>
<Dialog.Outer control={control} testID={testID}>
<Context.Provider value={context}>
<Dialog.Handle />
<Dialog.Inner
<Dialog.ScrollableInner
accessibilityLabelledBy={titleId}
accessibilityDescribedBy={descriptionId}
style={[{width: 'auto', maxWidth: 400}]}>
style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}>
{children}
</Dialog.Inner>
</Dialog.ScrollableInner>
</Context.Provider>
</Dialog.Outer>
)
@@ -71,8 +74,18 @@ export function Description({children}: React.PropsWithChildren<{}>) {
}
export function Actions({children}: React.PropsWithChildren<{}>) {
const {gtMobile} = useBreakpoints()
return (
<View style={[a.w_full, a.flex_row, a.gap_sm, a.justify_end]}>
<View
style={[
a.w_full,
a.gap_sm,
a.justify_end,
gtMobile
? [a.flex_row, a.flex_row_reverse, a.justify_start]
: [a.flex_col],
]}>
{children}
</View>
)
@@ -80,17 +93,29 @@ export function Actions({children}: React.PropsWithChildren<{}>) {
export function Cancel({
children,
}: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) {
cta,
}: React.PropsWithChildren<{
/**
* Optional i18n string, used in lieu of `children` for simple buttons. If
* undefined (and `children` is undefined), it will default to "Cancel".
*/
cta?: string
}>) {
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const {close} = Dialog.useDialogContext()
const onPress = React.useCallback(() => {
close()
}, [close])
return (
<Button
variant="solid"
color="secondary"
size="small"
label={_(msg`Cancel`)}
onPress={() => close()}>
{children}
size={gtMobile ? 'small' : 'medium'}
label={cta || _(msg`Cancel`)}
onPress={onPress}>
{children ? children : <ButtonText>{cta || _(msg`Cancel`)}</ButtonText>}
</Button>
)
}
@@ -98,21 +123,70 @@ export function Cancel({
export function Action({
children,
onPress,
}: React.PropsWithChildren<{onPress?: () => void}>) {
color = 'primary',
cta,
testID,
}: React.PropsWithChildren<{
onPress: () => void
color?: ButtonColor
/**
* Optional i18n string, used in lieu of `children` for simple buttons. If
* undefined (and `children` is undefined), it will default to "Confirm".
*/
cta?: string
testID?: string
}>) {
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const {close} = Dialog.useDialogContext()
const handleOnPress = React.useCallback(() => {
close()
onPress?.()
onPress()
}, [close, onPress])
return (
<Button
variant="solid"
color="primary"
size="small"
label={_(msg`Confirm`)}
onPress={handleOnPress}>
{children}
color={color}
size={gtMobile ? 'small' : 'medium'}
label={cta || _(msg`Confirm`)}
onPress={handleOnPress}
testID={testID}>
{children ? children : <ButtonText>{cta || _(msg`Confirm`)}</ButtonText>}
</Button>
)
}
export function Basic({
control,
title,
description,
cancelButtonCta,
confirmButtonCta,
onConfirm,
confirmButtonColor,
}: React.PropsWithChildren<{
control: Dialog.DialogOuterProps['control']
title: string
description: string
cancelButtonCta?: string
confirmButtonCta?: string
onConfirm: () => void
confirmButtonColor?: ButtonColor
}>) {
return (
<Outer control={control} testID="confirmModal">
<Title>{title}</Title>
<Description>{description}</Description>
<Actions>
<Action
cta={confirmButtonCta}
onPress={onConfirm}
color={confirmButtonColor}
testID="confirmBtn"
/>
<Cancel cta={cancelButtonCta} />
</Actions>
</Outer>
)
}
@@ -0,0 +1,89 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppBskyLabelerDefs} from '@atproto/api'
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import {Button, useButtonContext} from '#/components/Button'
import {Divider} from '#/components/Divider'
import * as LabelingServiceCard from '#/components/LabelingServiceCard'
import {ReportDialogProps} from './types'
export function SelectLabelerView({
...props
}: ReportDialogProps & {
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
onSelectLabeler: (v: string) => void
}) {
const t = useTheme()
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
return (
<View style={[a.gap_lg]}>
<View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>Select moderator</Trans>
</Text>
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
<Trans>To whom would you like to send this report?</Trans>
</Text>
</View>
<Divider />
<View style={[a.gap_sm]}>
{props.labelers.map(labeler => {
return (
<Button
key={labeler.creator.did}
label={_(msg`Send report to ${labeler.creator.displayName}`)}
onPress={() => props.onSelectLabeler(labeler.creator.did)}>
<LabelerButton labeler={labeler} />
</Button>
)
})}
</View>
</View>
)
}
function LabelerButton({
labeler,
}: {
labeler: AppBskyLabelerDefs.LabelerViewDetailed
}) {
const t = useTheme()
const {hovered, pressed} = useButtonContext()
const interacted = hovered || pressed
return (
<LabelingServiceCard.Outer
style={[
a.p_md,
a.rounded_sm,
t.atoms.bg_contrast_25,
interacted && t.atoms.bg_contrast_50,
]}>
<LabelingServiceCard.Avatar avatar={labeler.creator.avatar} />
<LabelingServiceCard.Content>
<LabelingServiceCard.Title
value={getLabelingServiceTitle({
displayName: labeler.creator.displayName,
handle: labeler.creator.handle,
})}
/>
<Text
style={[t.atoms.text_contrast_medium, a.text_sm, a.font_semibold]}>
@{labeler.creator.handle}
</Text>
</LabelingServiceCard.Content>
</LabelingServiceCard.Outer>
)
}
@@ -0,0 +1,186 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppBskyLabelerDefs} from '@atproto/api'
import {useReportOptions, ReportOption} from '#/lib/moderation/useReportOptions'
import {DMCA_LINK} from '#/components/ReportDialog/const'
import {Link} from '#/components/Link'
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
import {atoms as a, useTheme, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography'
import {
Button,
ButtonIcon,
ButtonText,
useButtonContext,
} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {
ChevronRight_Stroke2_Corner0_Rounded as ChevronRight,
ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft,
} from '#/components/icons/Chevron'
import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from '#/components/icons/SquareArrowTopRight'
import {ReportDialogProps} from './types'
export function SelectReportOptionView({
...props
}: ReportDialogProps & {
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
onSelectReportOption: (reportOption: ReportOption) => void
goBack: () => void
}) {
const t = useTheme()
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const allReportOptions = useReportOptions()
const reportOptions = allReportOptions[props.params.type]
const i18n = React.useMemo(() => {
let title = _(msg`Report this content`)
let description = _(msg`Why should this content be reviewed?`)
if (props.params.type === 'account') {
title = _(msg`Report this user`)
description = _(msg`Why should this user be reviewed?`)
} else if (props.params.type === 'post') {
title = _(msg`Report this post`)
description = _(msg`Why should this post be reviewed?`)
} else if (props.params.type === 'list') {
title = _(msg`Report this list`)
description = _(msg`Why should this list be reviewed?`)
} else if (props.params.type === 'feedgen') {
title = _(msg`Report this feed`)
description = _(msg`Why should this feed be reviewed?`)
}
return {
title,
description,
}
}, [_, props.params.type])
return (
<View style={[a.gap_lg]}>
{props.labelers?.length > 1 ? (
<Button
size="small"
variant="solid"
color="secondary"
shape="round"
label={_(msg`Go back to previous step`)}
onPress={props.goBack}>
<ButtonIcon icon={ChevronLeft} />
</Button>
) : null}
<View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
<Text style={[a.text_2xl, a.font_bold]}>{i18n.title}</Text>
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
{i18n.description}
</Text>
</View>
<Divider />
<View style={[a.gap_sm]}>
{reportOptions.map(reportOption => {
return (
<Button
key={reportOption.reason}
label={_(msg`Create report for ${reportOption.title}`)}
onPress={() => props.onSelectReportOption(reportOption)}>
<ReportOptionButton
title={reportOption.title}
description={reportOption.description}
/>
</Button>
)
})}
{(props.params.type === 'post' || props.params.type === 'account') && (
<View
style={[
a.flex_row,
a.align_center,
a.justify_between,
a.gap_lg,
a.p_md,
a.pl_lg,
a.rounded_md,
t.atoms.bg_contrast_900,
]}>
<Text
style={[
a.flex_1,
t.atoms.text_inverted,
a.italic,
a.leading_snug,
]}>
<Trans>Need to report a copyright violation?</Trans>
</Text>
<Link
to={DMCA_LINK}
label={_(msg`View details for reporting a copyright violation`)}
size="small"
variant="solid"
color="secondary">
<ButtonText>
<Trans>View details</Trans>
</ButtonText>
<ButtonIcon position="right" icon={SquareArrowTopRight} />
</Link>
</View>
)}
</View>
</View>
)
}
function ReportOptionButton({
title,
description,
}: {
title: string
description: string
}) {
const t = useTheme()
const {hovered, pressed} = useButtonContext()
const interacted = hovered || pressed
return (
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.justify_between,
a.p_md,
a.rounded_md,
{paddingRight: 70},
t.atoms.bg_contrast_25,
interacted && t.atoms.bg_contrast_50,
]}>
<View style={[a.flex_1, a.gap_xs]}>
<Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}>
{title}
</Text>
<Text style={[a.leading_tight, {maxWidth: 400}]}>{description}</Text>
</View>
<View
style={[
a.absolute,
a.inset_0,
a.justify_center,
a.pr_md,
{left: 'auto'},
]}>
<ChevronRight size="md" fill={t.atoms.text_contrast_low.color} />
</View>
</View>
)
}
+264
View File
@@ -0,0 +1,264 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppBskyLabelerDefs} from '@atproto/api'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {ReportOption} from '#/lib/moderation/useReportOptions'
import {atoms as a, useTheme, native} from '#/alf'
import {Text} from '#/components/Typography'
import * as Dialog from '#/components/Dialog'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import * as Toggle from '#/components/forms/Toggle'
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
import {Loader} from '#/components/Loader'
import * as Toast from '#/view/com/util/Toast'
import {ReportDialogProps} from './types'
import {getAgent} from '#/state/session'
export function SubmitView({
params,
labelers,
selectedLabeler,
selectedReportOption,
goBack,
onSubmitComplete,
}: ReportDialogProps & {
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
selectedLabeler: string
selectedReportOption: ReportOption
goBack: () => void
onSubmitComplete: () => void
}) {
const t = useTheme()
const {_} = useLingui()
const [details, setDetails] = React.useState<string>('')
const [submitting, setSubmitting] = React.useState<boolean>(false)
const [selectedServices, setSelectedServices] = React.useState<string[]>([
selectedLabeler,
])
const [error, setError] = React.useState('')
const submit = React.useCallback(async () => {
setSubmitting(true)
setError('')
const $type =
params.type === 'account'
? 'com.atproto.admin.defs#repoRef'
: 'com.atproto.repo.strongRef'
const report = {
reasonType: selectedReportOption.reason,
subject: {
$type,
...params,
},
reason: details,
}
const results = await Promise.all(
selectedServices.map(did =>
getAgent()
.withProxy('atproto_labeler', did)
.createModerationReport(report)
.then(
_ => true,
_ => false,
),
),
)
setSubmitting(false)
if (results.includes(true)) {
Toast.show(_(msg`Thank you. Your report has been sent.`))
onSubmitComplete()
} else {
setError(
_(
msg`There was an issue sending your report. Please check your internet connection.`,
),
)
}
}, [
_,
params,
details,
selectedReportOption,
selectedServices,
onSubmitComplete,
setError,
])
return (
<View style={[a.gap_2xl]}>
<Button
size="small"
variant="solid"
color="secondary"
shape="round"
label={_(msg`Go back to previous step`)}
onPress={goBack}>
<ButtonIcon icon={ChevronLeft} />
</Button>
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.justify_between,
a.gap_lg,
a.p_md,
a.rounded_md,
a.border,
t.atoms.border_contrast_low,
]}>
<View style={[a.flex_1, a.gap_xs]}>
<Text style={[a.text_md, a.font_bold]}>
{selectedReportOption.title}
</Text>
<Text style={[a.leading_tight, {maxWidth: 400}]}>
{selectedReportOption.description}
</Text>
</View>
<Check size="md" style={[a.pr_sm, t.atoms.text_contrast_low]} />
</View>
<View style={[a.gap_md]}>
<Text style={[t.atoms.text_contrast_medium]}>
<Trans>Select the moderation service(s) to report to</Trans>
</Text>
<Toggle.Group
label="Select mod services"
values={selectedServices}
onChange={setSelectedServices}>
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
{labelers.map(labeler => {
const title = getLabelingServiceTitle({
displayName: labeler.creator.displayName,
handle: labeler.creator.handle,
})
return (
<Toggle.Item
key={labeler.creator.did}
name={labeler.creator.did}
label={title}>
<LabelerToggle title={title} />
</Toggle.Item>
)
})}
</View>
</Toggle.Group>
</View>
<View style={[a.gap_md]}>
<Text style={[t.atoms.text_contrast_medium]}>
<Trans>Optionally provide additional information below:</Trans>
</Text>
<View style={[a.relative, a.w_full]}>
<Dialog.Input
multiline
value={details}
onChangeText={setDetails}
label="Text field"
style={{paddingRight: 60}}
numberOfLines={6}
/>
<View
style={[
a.absolute,
a.flex_row,
a.align_center,
a.pr_md,
a.pb_sm,
{
bottom: 0,
right: 0,
},
]}>
<CharProgress count={details?.length || 0} />
</View>
</View>
</View>
<View style={[a.flex_row, a.align_center, a.justify_end, a.gap_lg]}>
{!selectedServices.length ||
(error && (
<Text
style={[
a.flex_1,
a.italic,
a.leading_snug,
t.atoms.text_contrast_medium,
]}>
{error ? (
error
) : (
<Trans>You must select at least one labeler for a report</Trans>
)}
</Text>
))}
<Button
size="large"
variant="solid"
color="negative"
label={_(msg`Send report`)}
onPress={submit}
disabled={!selectedServices.length}>
<ButtonText>
<Trans>Send report</Trans>
</ButtonText>
{submitting && <ButtonIcon icon={Loader} />}
</Button>
</View>
</View>
)
}
function LabelerToggle({title}: {title: string}) {
const t = useTheme()
const ctx = Toggle.useItemContext()
return (
<View
style={[
a.flex_row,
a.align_center,
a.gap_md,
a.p_md,
a.pr_lg,
a.rounded_sm,
a.overflow_hidden,
t.atoms.bg_contrast_25,
ctx.selected && [t.atoms.bg_contrast_50],
]}>
<Toggle.Checkbox />
<View
style={[
a.flex_row,
a.align_center,
a.justify_between,
a.gap_lg,
a.z_10,
]}>
<Text
style={[
native({marginTop: 2}),
t.atoms.text_contrast_medium,
ctx.selected && t.atoms.text,
]}>
{title}
</Text>
</View>
</View>
)
}
+1
View File
@@ -0,0 +1 @@
export const DMCA_LINK = 'https://bsky.social/about/support/copyright'
+102
View File
@@ -0,0 +1,102 @@
import React from 'react'
import {Pressable, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ReportOption} from '#/lib/moderation/useReportOptions'
import {useMyLabelersQuery} from '#/state/queries/preferences'
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
import {AppBskyLabelerDefs} from '@atproto/api'
import {BottomSheetScrollViewMethods} from '@discord/bottom-sheet/src'
import {atoms as a} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {useDelayedLoading} from '#/components/hooks/useDelayedLoading'
import {useOnKeyboardDidShow} from '#/components/hooks/useOnKeyboard'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {SelectLabelerView} from './SelectLabelerView'
import {SelectReportOptionView} from './SelectReportOptionView'
import {SubmitView} from './SubmitView'
import {ReportDialogProps} from './types'
export function ReportDialog(props: ReportDialogProps) {
return (
<Dialog.Outer control={props.control}>
<Dialog.Handle />
<ReportDialogInner {...props} />
</Dialog.Outer>
)
}
function ReportDialogInner(props: ReportDialogProps) {
const {_} = useLingui()
const {
isLoading: isLabelerLoading,
data: labelers,
error,
} = useMyLabelersQuery()
const isLoading = useDelayedLoading(500, isLabelerLoading)
const ref = React.useRef<BottomSheetScrollViewMethods>(null)
useOnKeyboardDidShow(() => {
ref.current?.scrollToEnd({animated: true})
})
return (
<Dialog.ScrollableInner label={_(msg`Report dialog`)} ref={ref}>
{isLoading ? (
<View style={[a.align_center, {height: 100}]}>
<Loader size="xl" />
{/* Here to capture focus for a hot sec to prevent flash */}
<Pressable accessible={false} />
</View>
) : error || !labelers ? (
<View>
<Text style={[a.text_md]}>
<Trans>Something went wrong, please try again.</Trans>
</Text>
</View>
) : (
<ReportDialogLoaded labelers={labelers} {...props} />
)}
</Dialog.ScrollableInner>
)
}
function ReportDialogLoaded(
props: ReportDialogProps & {
labelers: AppBskyLabelerDefs.LabelerViewDetailed[]
},
) {
const [selectedLabeler, setSelectedLabeler] = React.useState<
string | undefined
>(props.labelers.length === 1 ? props.labelers[0].creator.did : undefined)
const [selectedReportOption, setSelectedReportOption] = React.useState<
ReportOption | undefined
>()
if (selectedReportOption && selectedLabeler) {
return (
<SubmitView
{...props}
selectedLabeler={selectedLabeler}
selectedReportOption={selectedReportOption}
goBack={() => setSelectedReportOption(undefined)}
onSubmitComplete={() => props.control.close()}
/>
)
}
if (selectedLabeler) {
return (
<SelectReportOptionView
{...props}
goBack={() => setSelectedLabeler(undefined)}
onSelectReportOption={setSelectedReportOption}
/>
)
}
return <SelectLabelerView {...props} onSelectLabeler={setSelectedLabeler} />
}
+15
View File
@@ -0,0 +1,15 @@
import * as Dialog from '#/components/Dialog'
export type ReportDialogProps = {
control: Dialog.DialogOuterProps['control']
params:
| {
type: 'post' | 'list' | 'feedgen' | 'other'
uri: string
cid: string
}
| {
type: 'account'
did: string
}
}
+6 -4
View File
@@ -59,7 +59,7 @@ export function TagMenu({
const displayTag = '#' + tag
const isMuted = Boolean(
(preferences?.mutedWords?.find(
(preferences?.moderationPrefs.mutedWords?.find(
m => m.value === tag && m.targets.includes('tag'),
) ??
optimisticUpsert?.find(
@@ -98,7 +98,7 @@ export function TagMenu({
control.close(() => {
navigation.push('Hashtag', {
tag: tag.replaceAll('#', '%23'),
tag: encodeURIComponent(tag),
})
})
@@ -153,7 +153,7 @@ export function TagMenu({
control.close(() => {
navigation.push('Hashtag', {
tag: tag.replaceAll('#', '%23'),
tag: encodeURIComponent(tag),
author: authorHandle,
})
})
@@ -264,7 +264,9 @@ export function TagMenu({
variant="ghost"
color="secondary"
onPress={() => control.close()}>
<ButtonText>Cancel</ButtonText>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
</>
)}
+4 -4
View File
@@ -50,7 +50,7 @@ export function TagMenu({
const {mutateAsync: removeMutedWord, variables: optimisticRemove} =
useRemoveMutedWordMutation()
const isMuted = Boolean(
(preferences?.mutedWords?.find(
(preferences?.moderationPrefs.mutedWords?.find(
m => m.value === tag && m.targets.includes('tag'),
) ??
optimisticUpsert?.find(
@@ -66,7 +66,7 @@ export function TagMenu({
label: _(msg`See ${truncatedTag} posts`),
onPress() {
navigation.push('Hashtag', {
tag: tag.replaceAll('#', '%23'),
tag: encodeURIComponent(tag),
})
},
testID: 'tagMenuSearch',
@@ -83,11 +83,11 @@ export function TagMenu({
label: _(msg`See ${truncatedTag} posts by user`),
onPress() {
navigation.push('Hashtag', {
tag: tag.replaceAll('#', '%23'),
tag: encodeURIComponent(tag),
author: authorHandle,
})
},
testID: 'tagMenuSeachByUser',
testID: 'tagMenuSearchByUser',
icon: {
ios: {
name: 'magnifyingglass',
+11 -6
View File
@@ -1,9 +1,14 @@
import React from 'react'
import {Text as RNText, TextStyle, TextProps as RNTextProps} from 'react-native'
import {
Text as RNText,
StyleProp,
TextStyle,
TextProps as RNTextProps,
} from 'react-native'
import {UITextView} from 'react-native-ui-text-view'
import {useTheme, atoms, web, flatten} from '#/alf'
import {isIOS} from '#/platform/detection'
import {isIOS, isNative} from '#/platform/detection'
export type TextProps = RNTextProps & {
/**
@@ -34,17 +39,17 @@ export function leading<
* If the `lineHeight` value is > 2, we assume it's an absolute value and
* returns it as-is.
*/
function normalizeTextStyles(styles: TextStyle[]) {
export function normalizeTextStyles(styles: StyleProp<TextStyle>) {
const s = flatten(styles)
// should always be defined on these components
const fontSize = s.fontSize || atoms.text_md.fontSize
if (s?.lineHeight) {
if (s.lineHeight <= 2) {
if (s.lineHeight !== 0 && s.lineHeight <= 2) {
s.lineHeight = Math.round(fontSize * s.lineHeight)
}
} else {
s.lineHeight = fontSize
} else if (!isNative) {
s.lineHeight = s.fontSize
}
return s
@@ -0,0 +1,132 @@
import React from 'react'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {View} from 'react-native'
import * as Dialog from '#/components/Dialog'
import {Text} from '../Typography'
import {DateInput} from '#/view/com/util/forms/DateInput'
import {logger} from '#/logger'
import {
usePreferencesQuery,
usePreferencesSetBirthDateMutation,
UsePreferencesQueryResponse,
} from '#/state/queries/preferences'
import {Button, ButtonIcon, ButtonText} from '../Button'
import {atoms as a, useTheme} from '#/alf'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {cleanError} from '#/lib/strings/errors'
import {isIOS, isWeb} from '#/platform/detection'
import {Loader} from '#/components/Loader'
export function BirthDateSettingsDialog({
control,
}: {
control: Dialog.DialogControlProps
}) {
const t = useTheme()
const {_} = useLingui()
const {isLoading, error, data: preferences} = usePreferencesQuery()
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner label={_(msg`My Birthday`)}>
<View style={[a.gap_sm, a.pb_lg]}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>My Birthday</Trans>
</Text>
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
<Trans>This information is not shared with other users.</Trans>
</Text>
</View>
{isLoading ? (
<Loader size="xl" />
) : error || !preferences ? (
<ErrorMessage
message={
error?.toString() ||
_(
msg`We were unable to load your birth date preferences. Please try again.`,
)
}
style={[a.rounded_sm]}
/>
) : (
<BirthdayInner control={control} preferences={preferences} />
)}
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
function BirthdayInner({
control,
preferences,
}: {
control: Dialog.DialogControlProps
preferences: UsePreferencesQueryResponse
}) {
const {_} = useLingui()
const [date, setDate] = React.useState(preferences.birthDate || new Date())
const {
isPending,
isError,
error,
mutateAsync: setBirthDate,
} = usePreferencesSetBirthDateMutation()
const hasChanged = date !== preferences.birthDate
const onSave = React.useCallback(async () => {
try {
// skip if date is the same
if (hasChanged) {
await setBirthDate({birthDate: date})
}
control.close()
} catch (e: any) {
logger.error(`setBirthDate failed`, {message: e.message})
}
}, [date, setBirthDate, control, hasChanged])
return (
<View style={a.gap_lg} testID="birthDateSettingsDialog">
<View style={isIOS && [a.w_full, a.align_center]}>
<DateInput
handleAsUTC
testID="birthdayInput"
value={date}
onChange={setDate}
buttonType="default-light"
buttonStyle={[a.rounded_sm]}
buttonLabelType="lg"
accessibilityLabel={_(msg`Birthday`)}
accessibilityHint={_(msg`Enter your birth date`)}
accessibilityLabelledBy="birthDate"
/>
</View>
{isError ? (
<ErrorMessage message={cleanError(error)} style={[a.rounded_sm]} />
) : undefined}
<View style={isWeb && [a.flex_row, a.justify_end]}>
<Button
label={hasChanged ? _(msg`Save birthday`) : _(msg`Done`)}
size="medium"
onPress={onSave}
variant="solid"
color="primary">
<ButtonText>
{hasChanged ? <Trans>Save</Trans> : <Trans>Done</Trans>}
</ButtonText>
{isPending && <ButtonIcon icon={Loader} />}
</Button>
</View>
</View>
)
}
+1 -1
View File
@@ -18,7 +18,7 @@ export function useGlobalDialogsControlContext() {
export function Provider({children}: React.PropsWithChildren<{}>) {
const mutedWordsDialogControl = Dialog.useDialogControl()
const ctx = React.useMemo(
const ctx = React.useMemo<ControlsContext>(
() => ({mutedWordsDialogControl}),
[mutedWordsDialogControl],
)
+14 -27
View File
@@ -233,8 +233,8 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
</Trans>
</Text>
</View>
) : preferences.mutedWords.length ? (
[...preferences.mutedWords]
) : preferences.moderationPrefs.mutedWords.length ? (
[...preferences.moderationPrefs.mutedWords]
.reverse()
.map((word, i) => (
<MutedWordRow
@@ -254,9 +254,9 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
</View>
{isNative && <View style={{height: 20}} />}
<Dialog.Close />
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
)
}
@@ -277,29 +277,16 @@ function MutedWordRow({
return (
<>
<Prompt.Outer control={control}>
<Prompt.Title>
<Trans>Are you sure?</Trans>
</Prompt.Title>
<Prompt.Description>
<Trans>
This will delete {word.value} from your muted words. You can always
add it back later.
</Trans>
</Prompt.Description>
<Prompt.Actions>
<Prompt.Cancel>
<ButtonText>
<Trans>Nevermind</Trans>
</ButtonText>
</Prompt.Cancel>
<Prompt.Action onPress={remove}>
<ButtonText>
<Trans>Remove</Trans>
</ButtonText>
</Prompt.Action>
</Prompt.Actions>
</Prompt.Outer>
<Prompt.Basic
control={control}
title={_(msg`Are you sure?`)}
description={_(
msg`This will delete ${word.value} from your muted words. You can always add it back later.`,
)}
onConfirm={remove}
confirmButtonCta={_(msg`Remove`)}
confirmButtonColor="negative"
/>
<View
style={[
@@ -1,20 +1,11 @@
import React from 'react'
import {View, Pressable} from 'react-native'
import DateTimePicker, {
BaseProps as DateTimePickerProps,
} from '@react-native-community/datetimepicker'
import {useTheme, atoms} from '#/alf'
import {Text} from '#/components/Typography'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import * as TextField from '#/components/forms/TextField'
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
import DatePicker from 'react-native-date-picker'
import {useTheme} from '#/alf'
import {DateFieldProps} from '#/components/forms/DateField/types'
import {
localizeDate,
toSimpleDateString,
} from '#/components/forms/DateField/utils'
import {toSimpleDateString} from '#/components/forms/DateField/utils'
import * as TextField from '#/components/forms/TextField'
import {DateFieldButton} from './index.shared'
export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label
@@ -25,84 +16,55 @@ export function DateField({
label,
isInvalid,
testID,
accessibilityHint,
}: DateFieldProps) {
const t = useTheme()
const [open, setOpen] = React.useState(false)
const {
state: pressed,
onIn: onPressIn,
onOut: onPressOut,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const {chromeFocus, chromeError, chromeErrorHover} =
TextField.useSharedInputStyles()
const onChangeInternal = React.useCallback<
Required<DateTimePickerProps>['onChange']
>(
(_event, date) => {
const onChangeInternal = React.useCallback(
(date: Date) => {
setOpen(false)
if (date) {
const formatted = toSimpleDateString(date)
onChangeDate(formatted)
}
const formatted = toSimpleDateString(date)
onChangeDate(formatted)
},
[onChangeDate, setOpen],
)
return (
<View style={[atoms.relative, atoms.w_full]}>
<Pressable
aria-label={label}
accessibilityLabel={label}
accessibilityHint={undefined}
onPress={() => setOpen(true)}
onPressIn={onPressIn}
onPressOut={onPressOut}
onFocus={onFocus}
onBlur={onBlur}
style={[
{
paddingTop: 16,
paddingBottom: 16,
borderColor: 'transparent',
borderWidth: 2,
},
atoms.flex_row,
atoms.flex_1,
atoms.w_full,
atoms.px_lg,
atoms.rounded_sm,
t.atoms.bg_contrast_50,
focused || pressed ? chromeFocus : {},
isInvalid ? chromeError : {},
isInvalid && (focused || pressed) ? chromeErrorHover : {},
]}>
<TextField.Icon icon={CalendarDays} />
const onPress = React.useCallback(() => {
setOpen(true)
}, [])
<Text
style={[atoms.text_md, atoms.pl_xs, t.atoms.text, {paddingTop: 3}]}>
{localizeDate(value)}
</Text>
</Pressable>
const onCancel = React.useCallback(() => {
setOpen(false)
}, [])
return (
<>
<DateFieldButton
label={label}
value={value}
onPress={onPress}
isInvalid={isInvalid}
accessibilityHint={accessibilityHint}
/>
{open && (
<DateTimePicker
<DatePicker
modal
open
timeZoneOffsetInMinutes={0}
theme={t.name === 'light' ? 'light' : 'dark'}
date={new Date(value)}
onConfirm={onChangeInternal}
onCancel={onCancel}
mode="date"
testID={`${testID}-datepicker`}
aria-label={label}
accessibilityLabel={label}
accessibilityHint={undefined}
testID={`${testID}-datepicker`}
mode="date"
timeZoneName={'Etc/UTC'}
display="spinner"
// @ts-ignore applies in iOS only -prf
themeVariant={t.name === 'light' ? 'light' : 'dark'}
value={new Date(value)}
onChange={onChangeInternal}
accessibilityHint={accessibilityHint}
/>
)}
</View>
</>
)
}

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