Compare commits

...

395 Commits

Author SHA1 Message Date
Eric Bailey 5f49d1a686 Add share button to post controls 2023-12-18 15:22:37 -06:00
bnewbold 3e3a72a366 basic public RSS feed for profiles (#2229)
* web: initial implementation of profile RSS feed

* re-work RSS feed to use DID in URL, not handle

Shouldn't have RSS feeds break when folks change handle.

* rss: tweak XML
2023-12-18 23:52:39 +04:00
Samuel Newman edc6bdb4d6 use s.likeColor everywhere (#2234) 2023-12-18 09:47:17 -08:00
Ansh 81fefabe1a Add credits to localization doc (#2233)
* change name of file to localization

* run `intt:build`
2023-12-18 09:24:31 -08:00
Eric Bailey 9ff1828437 Fix Logotype fill for dark mode (#2230)
* Fix fill of Logotype in dark mode

* Update default Logotype fill
2023-12-18 09:20:58 -08:00
Eric Bailey 47ff3e8e3f Hide label appeal on self-labeled posts (#2232)
* Hide appeal for self-labeled post, update copy

* Use correct logic
2023-12-18 09:16:33 -08:00
Hima 38e65fbe75 Fix & Add: Japanese Translation (3) (#2226) 2023-12-18 22:43:20 +05:30
bnewbold 8da5a375a5 tweak social card meta yet again (#2228)
- handle missing content (PWI opt-out) better
- use opengraph "article" and "profile" types, not just website for everything
- fix timestamps, and used indexed at not created at
2023-12-18 20:24:25 +04:00
bnewbold 0c47f6dc98 bskyweb: update golang indigo dep (from May!), and some small devex tweaks (#2227)
* bskyweb: update deps to current indigo

* bskyweb: refactor handles to flatten control flow, and update to current indigo

* go Makefile: remove --debug (not working)

* bskyweb: default to public appview API instance

* make: do internationalization build as part of build-web
2023-12-18 19:31:48 +04:00
Hima b942da162a Fix: Some display issues (#2219)
* Update LeftNav.tsx

* Update RightNav.tsx
2023-12-18 18:28:58 +05:30
Paul Frazee 9f82441c37 Update splash screen tagline, update translations, bump ios build number 2023-12-17 15:28:04 -08:00
Paul Frazee 77437194e0 Bump ios build number 2023-12-17 11:57:23 -08:00
Eric Bailey 25f0527ad6 Tweaks (#2225)
* Gradient

* Tweak animation speed

* Rotate

* Linear

* Linear
2023-12-17 11:55:47 -08:00
Paul Frazee aac13aafce 1.60 2023-12-15 15:57:06 -08:00
Paul Frazee 2a712630b4 Traffic reduction and tuned caching strats (#2215)
* Update the feed to only check latest on focus after 30s, but to do a full reset on focus after 1 hour to avoid very stale data

* Remove the isFeedPublic query

* Fix: avoid double next-page fetches

* Reduce some poll intervals to reduce server load

* Guard against double-fires of fetchNextPage

* Reduce polling on blurred screens
2023-12-15 15:49:07 -08:00
Eric Bailey dd074371cf Super secret changes don't look (#2218)
* Swap in new assets

* Refine
2023-12-15 15:40:22 -08:00
Eric Bailey ce6d3fa0e1 Fix android icon dims (#2213) 2023-12-14 15:55:53 -08:00
Eric Bailey a0d006a1ac Log post creation failures (#2205) 2023-12-14 15:55:35 -08:00
Eric Bailey 1111108efe Some brand, some pwi (#2212)
* Add logo to left nav in logged out

* Protect last routes

* Hide links in left nav, hide nav

* Replace bottom bar for pwi

* Remove same links from drawer

* Hide reply prompt

* Allow search
2023-12-14 15:46:27 -08:00
Eric Bailey 7897dd24a1 🤫 (#2211)
* Add new assets

* Add splashiness

* Add butter icon, spread it

* Cream together eggs, sugar, and vanilla

* Hi, I'd like to place and order. Yeah, none pizza with left beef, plz.

* test

* Refine animation

* tweak

* tweak

* tweak

* Tweak

* Simplify

* Cleanup

* Fix android logo

---------

Co-authored-by: Ansh Nanda <anshnanda10@gmail.com>
2023-12-14 14:48:40 -06:00
Paul Frazee 075ffdf583 PWI behavior updates (#2207)
* Enable PWI

* Disable access to feeds on PWI

* Remove feeds nav item from drawer when signed out

* Replace discover feed on home with a CTA

* Wire up the sign in and create account buttons to go straight to their respective screens

* Give a custom ScreenHider interface for no-pwi

* Add side borders on desktop to the screen hider

* Filter accounts in the autocomplete according to mod settings

* Trim replies in the post thread that are pwi opt-out

* Show 'learn more' on the content hider when no-override is enabled

* Apply the moderation filter on profile cards

* Disable post search on logged-out view

* Update locale files

* Bump api pkg

* Ensure feeds with no posts don't show as NSFPublic

* Fix types

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2023-12-14 10:31:49 -08:00
dan 7fd7970237 Make scroll handling contextual (#2200)
* Add an intermediate List component

* Fix type

* Add onScrolledDownChange

* Port pager to use onScrolledDownChange

* Fix on mobile

* Don't pass down onScroll (replacement TBD)

* Remove resetMainScroll

* Replace onMainScroll with MainScrollProvider

* Hook ScrollProvider to pager

* Fix the remaining special case

* Optimize a bit

* Enforce that onScroll cannot be passed

* Keep value updated even if no handler

* Also memo it
2023-12-13 18:48:20 -08:00
noriaki watanabe fa3ccafa80 Add i18n setup in build instructions (#2206) 2023-12-13 18:17:59 -08:00
Paul Frazee 5fa43530f6 Revert change to how notifications are loaded on mobile 2023-12-13 13:48:49 -08:00
Paul Frazee a713f77e03 1.59 2023-12-13 12:48:26 -08:00
Paul Frazee 8245e56747 Feed bugfixes (#2204)
* Dont show both an error and empty message in the feed

* Add a sanity check when attempting to fill the first 30
2023-12-13 12:17:14 -08:00
Paul Frazee e3ba014be0 More notifications improvements (#2198)
* On mobile, never replace the notifs under the user due to focus events

* Use the server's seenAt response to calculate isRead state locally
2023-12-13 12:16:55 -08:00
Paul Frazee eecf04489f Rework the rightnav overflow behavior to not obscure the scroll region (#2199) 2023-12-13 09:59:27 -08:00
Ansh 001bbb41bb Hotfix: Merge conflict in translation file (#2203)
* remove unused translation files

* re-run extract and compile
2023-12-13 19:48:53 +05:30
dan 0e3218db7d Memoize usePalette (#2201) 2023-12-13 07:09:07 +00:00
Eric Bailey b1f9454f1d Add fallback to email reminder modal (#2193)
* Add fallback to email reminder modal

* Add more debug
2023-12-12 15:18:38 -08:00
dan bf9cedb67b Clean up pager refs to fix crash (#2195) 2023-12-12 15:11:20 -08:00
dan 5c701f8e0b Re-rendering improvements for like/unlike (#2180)
* Add a few memos

* Memo PostDropdownBtn better

* More memo

* More granularity

* Extract PostContent

* Fix a usage I missed

* oops
2023-12-12 13:50:43 -08:00
Eric Bailey a5e25a7a16 Add largeHeap to AndroidManifest.xml (#2191) 2023-12-12 13:40:24 -08:00
Jaz bc42747297 Don't create embeds of profiles/posts from users who ask not to be sh… (#2189)
* Don't create embeds of profiles/posts from users who ask not to be shown in public views

* Formatting cleanup

* Bump workflow file to build an image for this branch
2023-12-12 13:10:13 -08:00
Ansh 9ab0ff6f1d add scrollview to moderation screen (#2187) 2023-12-12 13:07:59 -08:00
Ansh 1289b161a1 disable modal profile popup android (#2188) 2023-12-12 13:07:30 -08:00
Paul Frazee 870505cbe2 Go back to content-driven slice keys in feeds (#2190)
* Go back to deterministic react keys

* Quick fix to cases when custom feeds serve empty responses
2023-12-12 13:07:09 -08:00
Ansh 90647fe7cf Japanese localization (#2003)
* add japanese to list of languages

* add japanese translations

* change `jp` to `ja`

* minor changes to Japanese translation

* minor changes to Japanese translation

* update japanese strings with feedback

* Update languages.ts

* Extract translations

* Gitignore new compiled file

* allow switching to japanese

---------

Co-authored-by: Jake Gold <jake@blueskyweb.xyz>
Co-authored-by: Eric Bailey <git@esb.lol>
2023-12-13 01:12:19 +05:30
dan 27561ae79f Fix View Full Thread (#2181) 2023-12-12 11:31:02 -08:00
Eric Bailey 2f8e7b2656 Make right nav scrollable on short screens (#2186) 2023-12-12 11:20:06 -08:00
Eric Bailey e7141a77d8 Stub broadcast channel for unspported contexts (#2175) 2023-12-12 11:19:27 -08:00
Eric Bailey c6ab6e8b8e i18n settings improvements (#2184)
* Handle language selector

* Improve type safety

* Add a little more safety

* Update comment
2023-12-12 12:42:11 -06:00
Paul Frazee d82b1a1047 1.58 2023-12-11 15:52:56 -08:00
Paul Frazee c5f8909ecf Double the footer on the list members flatlist 2023-12-11 15:52:00 -08:00
Eric Bailey fc85901a46 Fix links in profiles (#2178)
* Resolve links in profile bios

* Improve solution

* On mobile, dont disable pointer events on the bio richtext

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-12-11 15:34:25 -08:00
Eric Bailey bae63f9bf0 Patch bad migration, sanitize bad value (#2179) 2023-12-11 15:34:18 -08:00
Paul Frazee 3592f6f50b Hide the mobile reply control if outside the threadgate (#2177) 2023-12-11 15:11:36 -08:00
Eric Bailey 9a59525b73 Revise FeedSourceCard load state to allow deletion for failures (#2172)
* Revise FeedSourceCard load state to allow deletion for failures

* Translate

* Remove test error feed

* Fix missing uri param

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-12-11 14:48:22 -08:00
Eric Bailey 586c2417a8 Handle failed pinned feed fetches when rendering list of pinned feeds (#2173) 2023-12-11 14:31:05 -08:00
dan 52ffd6aabb Don't validate embeds when traversing cache (#2174) 2023-12-11 14:30:32 -08:00
Ansh 0f7dbbba4a dismiss keyboard when threadgate settings are opened (#2161) 2023-12-11 13:55:40 -08:00
Eric Bailey f7ec7f700e Clarify MyList filter intent (#2167) 2023-12-11 13:55:14 -08:00
Paul Frazee 99cf6b626f Additional reductions in request traffic (#2169)
* Dont poll for new content on profiles

* Drop the whenAppReady query after new post to reduce traffic overhead

* Reduce getPosts calls in notifs to only use them when needed
2023-12-11 13:53:37 -08:00
dan ab04074197 Fix scroll on native (#2170) 2023-12-11 13:47:41 -08:00
dan 8929ff526f Fix scroll on profile lists/feeds (#2168) 2023-12-11 13:24:31 -08:00
Paul Frazee b82c5177b9 Reduce calls to getFeedGenerator and getFeed (#2166)
* Reduce calls to getFeedGenerator by the mergefeed

* Dont run end-of-follows mergefeed requests until actually at the end of the feed

* build AWS container

---------

Co-authored-by: Jake Gold <jake@blueskyweb.xyz>
2023-12-11 12:58:34 -08:00
Paul Frazee 3d229b5fd6 Bump ios and android build numbers 2023-12-10 12:25:14 -08:00
Paul Frazee f46b598ac6 Fix mock-server init 2023-12-10 12:04:24 -08:00
Paul Frazee 28fa5e4919 Add "Who can reply" controls [WIP] (#1954)
* Add threadgating

* UI improvements

* More ui work

* Remove comment

* Tweak colors

* Add missing keys

* Tweak sizing

* Only show composer option on non-reply

* Flex wrap fix

* Move the threadgate control to the top of the composer
2023-12-10 12:01:34 -08:00
Paul Frazee f5d014d4c7 Bump ios build 2023-12-09 17:21:03 -08:00
Eric Bailey 940a627996 Fix empty state width (#2157) 2023-12-09 15:10:53 -08:00
Eric Bailey 69a0a0bc41 Remove (#2156) 2023-12-09 15:10:29 -08:00
Eric Bailey 97b54b51c6 Add back isInitialLoad to session (#2155) 2023-12-09 15:10:12 -08:00
Paul Frazee 6b3eb401b0 Multiple notifications fixes (#2154)
* Dont reset notifications feed on push notification event

* Dont separate notifications by read state to avoid jank

* On notifications screen focus, check latest and only rerender if not scrolled down

* Reuse the cached notifs page when its not stale

* Bump ios build number

* Improve comments

* Change the 'mark all read' condition to avoid firing too early
2023-12-09 15:09:31 -08:00
Paul Frazee d854e88218 Bump ios build number 2023-12-08 16:31:08 -08:00
Paul Frazee 102094b10a Poll for new posts on app foreground (#2152) 2023-12-08 16:30:19 -08:00
Eric Bailey 9c0c18d5d0 Use new filter for Posts tab on profile (#2150)
* Use new filter for Posts tab on profile

* Update deps

* Remove log
2023-12-08 16:29:53 -08:00
Paul Frazee e1b1720cad Tweak the PWI opt-out copy again (#2153)
* Tweak the PWI ot-out copy again

* Update .po files
2023-12-08 16:26:20 -08:00
Eric Bailey 0270ada908 Fix quote post moderation check (#2149)
* Fix quote post moderation check

* Restore the ContentHider check

* Apply the content hider to quote posts

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-12-08 16:15:07 -08:00
Eric Bailey 1b8b1b2ed1 Await app view when handling blocks mutes for lists (#2151)
* Await app view when handling blocks mutes for lists

* Do it right this time
2023-12-08 15:55:35 -08:00
Paul Frazee 565979e71f Merge branch 'main' of github.com:bluesky-social/social-app into main 2023-12-08 14:01:20 -08:00
dan 7b686b5592 Add manual per-page memoization to post select (#2146) 2023-12-08 13:57:00 -08:00
Eric Bailey 61fa3d506c Close account switcher before logging out (#2148)
* Close account switcher before logging out

* Add comment
2023-12-08 13:00:19 -06:00
Ansh 528b4b5017 Clear unused locales (#2147)
* add some missing intl strings

* remove locales that are not used
2023-12-09 00:15:05 +05:30
Paul Frazee 282879b645 Bump ios build number 2023-12-07 17:21:10 -08:00
Paul Frazee 7b50331188 Various search fixes (#2145)
* Add posts-search query to shadow cache search

* Update user search to use correct endpoint

* Fix: include cursor in post search
2023-12-07 17:20:17 -08:00
Paul Frazee 448a403c81 Stop using the cached notification page (#2142) 2023-12-07 17:16:39 -08:00
Paul Frazee 7de5c341a9 Update pwi opt-out language (#2143) 2023-12-07 17:16:23 -08:00
dan 00731f62fe Fix bad merge in translations (#2144) 2023-12-07 17:08:23 -08:00
Ansh 9d51886e43 Fixes issue with (#2119)
* Allow going directly to password input screen when switching accounts and password is required

* Revise state handling

* Handle logged out states, enable clearing requestedAccount

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2023-12-07 16:53:50 -08:00
Eric Bailey afca4bf701 Add more breadcrumbs (#2141) 2023-12-07 16:48:23 -08:00
Eric Bailey 1975f4936a Fix invites used order, adjust layout (#2139) 2023-12-07 16:30:41 -08:00
Paul Frazee 040ce03215 Grab-bag of post-feed improvements (#2140)
* Sanity check against cases where empty pages may occur

* Use the mergefeed as an emergency fallback to an empty feed

* Check for new posts on focus
2023-12-07 16:30:04 -08:00
Paul Frazee 52a0cb8fac Add label appeal tool to posts and accounts (#2124)
* Add label appeal tool to posts and accounts

* Fix translations
2023-12-07 14:45:50 -08:00
dan 794015aef8 Navigate Home on account switch (#2138) 2023-12-07 14:28:49 -08:00
Eric Bailey 1b5989ac51 Allow blocking/muting to curational lists, unpinning to modlists (#2121)
* Add un-pinning of modlist

* Allow muting/blocking of curational lists

* Tweak icon

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-12-07 14:00:09 -08:00
Eric Bailey b7a5294123 Improve account switching on mobile (#2135)
* Improve account switching on mobile

* Clarify comment
2023-12-07 13:44:59 -08:00
Eric Bailey 174a1622c9 Hoist moderation, attempt to fill feed up to 30 (#2134)
* Move moderatePost up to feed query

* Attemt to fill page up to 30

* Add the 'ensure full page' behavior to notifs

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-12-07 13:44:22 -08:00
Paul Frazee 940fc0ea5c Disable animation on scrollToTop for web (#2137) 2023-12-07 13:41:02 -08:00
Eric Bailey f115969f50 Fix double sep (#2136) 2023-12-07 13:06:41 -08:00
Paul Frazee aeafa3a8cd Fix: remove replies to deleted and blocked posts from feeds (#2130) 2023-12-07 13:04:22 -08:00
Paul Frazee 972bc1bd88 Increase the maximum ratio of images from 5:1 to 10:1 (#2131) 2023-12-07 13:03:34 -08:00
Paul Frazee 5b19e2b90b Completely disable the RQ cache in post-thread (#2133) 2023-12-07 13:02:51 -08:00
Eric Bailey cdc1d08267 Apply color theme to HTML page itself (#2132) 2023-12-07 11:00:44 -08:00
Paul Frazee 17c27581b6 Add PWI opt-out toggle (#2122)
* Add PWI opt-out toggle

* Bump @atproto/api@0.7.0

* Tweak copy

* Bump lockfile

* Fix layout on ios

* Tweak copy more

* Fix types

* Tweak copy some more
2023-12-07 10:32:55 -08:00
Eric Bailey 261a935747 More session improvements (#2129)
* More session improvements

* Drop resume session retries from 3 to 1

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-12-07 10:25:55 -08:00
Eric Bailey a0b9fd799b Disable sentry in dev (#2128) 2023-12-07 09:46:33 -06:00
Paul Frazee 80ada4d20a Increment ios build number 2023-12-06 22:18:26 -08:00
Paul Frazee 00e1e56a7d Treeview: more UI tweaks (#2125)
* Treeview: more UI tweaks

* Only show treeview when there are branching replies

* Fix types
2023-12-06 22:17:38 -08:00
Eric Bailey 818c6ae879 Add tests for migration and persisted state (#2118)
* Add tests for migrate

* Add test for persisted.init

* Add legacy transform test

* Set NODE_ENV for testing

* Mock logger

* Set expo var to test
2023-12-06 16:41:05 -08:00
dan 07fe058577 Move analytics out of critical path (#2117)
* Remove analytics provider, simplify hook

* Fix wrong import being used by feed

* Remove early bind

* Create client lazy on first use
2023-12-06 13:06:54 -08:00
Eric Bailey a924df4dcd Refetch some data on an interval (#2114)
* Match refetch intervals to stale time

* Lower refetch interval for own-profile
2023-12-06 12:31:54 -08:00
Paul Frazee 47771b9066 Update pwi endpoint (#2116) 2023-12-06 12:20:39 -08:00
dan 6335be14e1 Move analytics out of init (#2115)
* Remove listenSessionLoaded from analytics

* Move analytics init call to navigation ready

* Remove zod dependency from analytics

* Mirror changes on the web

* Delete listenSessionLoaded

* Only set up listeners once
2023-12-06 12:04:05 -08:00
dan 748212e000 Remove getSession waterfall (#2112) 2023-12-06 18:32:14 +00:00
dan 20a46c8cff Enable structural sharing for preference query (#2113) 2023-12-06 10:20:23 -08:00
dan de38595a7a Split notification init, add cleanup (#2102)
* Inline never-imported export

* Remove pretense at handling cleanup

* Extract per-session listener to a function

* Split notifications.init() from notifications.onSessionLoaded()

* Tweak forked code to be more similar

* Remove unnecessary guards

* Split notifications.onSessionLoaded in two

* Always use getAgent()

* Remove dep on listenSessionLoaded, add cleanup
2023-12-06 10:05:59 -08:00
dan 7d158f82fb Optimize Drawer re-renders (#2108) 2023-12-06 09:50:06 -08:00
Ansh 8e541d753a Check Analytics (#2106)
* fix sign in event tracking

* add missing analytics events

* add more missing analytics

* fix like and unrepost event tracking

* reset onEndReachedThreshold
2023-12-06 09:45:01 -08:00
dan 7229cda5a5 Throttle non-critical Sentry messages (#2110)
* Throttle non-critical Sentry messages

* Run all timers in tests
2023-12-06 16:32:47 +00:00
Paul Frazee df55c5fdeb Bump ios build number 2023-12-05 22:57:47 -08:00
Paul Frazee 2571432875 Disable react-native-screen freezing to test performance implications (#2109) 2023-12-05 22:56:20 -08:00
Paul Frazee 3107bb07c9 Bump ios build number 2023-12-05 21:07:20 -08:00
Ansh 78e459d06f Add missing translations and fix react key error in MyLists.tsx (#2103)
* add missing translations

* fix key errors in MyLists

* Update UserAddRemoveLists.tsx
2023-12-05 18:27:38 -08:00
Eric Bailey e6bda92b20 Surface raw server error if exists (#2096)
* Surface raw server error if exists

* Update copy

* Update translation files

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-12-05 18:25:32 -08:00
Eric Bailey 7f3324d4a4 Improve error handling in dev-only (#2095) 2023-12-05 18:23:48 -08:00
Eric Bailey 570b78c32e Fix self-threads filter logic (#2091) 2023-12-05 18:22:06 -08:00
Paul Frazee 41fd6eb95b Fix some types 2023-12-05 18:21:15 -08:00
Paul Frazee 2ad0d059ac More treeview UI tweaks (#2093)
* Improve tree-view spacing consistency and always include one reply bar level

* Reduce expanded post avi size
2023-12-05 18:18:53 -08:00
Paul Frazee 511d5d999b Update to actions/setup-node@v4 (#2105)
* Update to actions/setup-node@v4

* Add a retry to the yarn install step
2023-12-05 18:18:28 -08:00
Paul Frazee 712cd3fde5 Perf: Include quote posts in the post-thread placeholder (#2104) 2023-12-05 18:17:03 -08:00
Paul Frazee accb25ccf2 Perf: Dont initial-render full thread on mobile (#2101) 2023-12-05 18:02:33 -08:00
Paul Frazee 826b841e10 Fix: correctly identify if the screen is focused when handling soft resets on post feeds (#2100) 2023-12-05 18:01:08 -08:00
Eric Bailey 3c8036587e Improvements to persisted state migration (#2098)
* Fix session email/emailConfirmed types, update usage for safer access

* Handle fallback better, better errors

* Better handling, add test

* Add test for default data

* Remove fallback, not needed, update logs
2023-12-05 19:59:34 -06:00
Eric Bailey a915a57b10 Prevent invalid locale from being loaded (#2099) 2023-12-05 17:51:50 -08:00
dan fab9f839d0 Fix email confirmation dialog on startup (#2097)
* Move init time logging to a fn, add guard

* Fix email confirmation dialog on startup
2023-12-06 00:28:11 +00:00
Paul Frazee e7f973852f Signup flow minor UI fixes (#2094)
* Fix layout of no-policy notice

* Fix spacing
2023-12-05 13:35:42 -08:00
Eric Bailey 5f553c29df E2E 🟢 (#2092)
* Add logged out e2e ctrl, fix login test

* Fix log handling via env vars in expo

* Fix create account test

* Upgrade dev-env

* Fix home screen tests

* Fix composer tests

* Fix curate-lists tests, split in two

* Fix invite codes test

* Fix curate-lists tests

* Give up on mergefeed test

* Fix mod lists

* Fix app view url

* Fix profile tests

* Fix profile test with hack

* Keep using globals

* Fix two more

* Fix thread view

* Better skip for merge feed

* Revert debug code
2023-12-05 14:50:56 -06:00
dan ed5a97d0fa Fix jump when toggling suggestions (#2090) 2023-12-05 09:13:09 -08:00
Paul Frazee 37d94ca0e3 Bump ios build number 2023-12-04 18:01:49 -08:00
Paul Frazee 48f5cebc80 Update feeds on post created (#2085) 2023-12-04 17:58:45 -08:00
Paul Frazee bdb2bfdd83 Rename Posts & Replies to Replies (#2087) 2023-12-04 17:56:41 -08:00
Paul Frazee 74e0b73db5 Add back ignore-filters-for behaviors (#2086) 2023-12-04 17:54:39 -08:00
Paul Frazee d07ea64434 Fix: correctly partial-match when calling truncateAndInvalidate() (#2088) 2023-12-04 17:53:22 -08:00
Paul Frazee 160055e4f9 Merge branch 'CooperEdmunds-2047-last-active-tab' into main 2023-12-04 16:33:18 -08:00
Paul Frazee 81c525f2b2 Merge branch '2047-last-active-tab' of https://github.com/CooperEdmunds/social-app into CooperEdmunds-2047-last-active-tab 2023-12-04 16:30:08 -08:00
Paul Frazee f1b69a332a Tune treeview thread mode to have consistent spacing (#2078) 2023-12-04 16:25:17 -08:00
Eric Bailey 4f171be9bf Fix bottom space on feeds (#2081)
* Fix bottom space on feeds

* Translate

* Handle web better
2023-12-04 16:24:51 -08:00
Paul Frazee 6e2eaa746d Optimistically update the pager tab bar on swipe (#2083) 2023-12-04 16:06:15 -08:00
Paul Frazee 8e2cfa52de Typefix 2023-12-04 15:08:34 -08:00
Eric Bailey f1cfea5786 Add back pinning functionality to ProfileList (#2084) 2023-12-04 15:52:51 -06:00
Eric Bailey 094a58490f Handle feedgen 429 (#2066)
* Handle feedgen 429

* Translate
2023-12-04 13:27:48 -08:00
Cooper Edmunds 17c58d40ff Track active page using string instead of index 2023-12-04 16:10:03 -05:00
Paul Frazee 37cafb080b Fix: distinguish between post media and quotes with the moderation hider (#2075)
* Fix: distinguish between post media and quotes with the moderation hider

* Type fixes
2023-12-04 12:53:25 -08:00
Paul Frazee a46059ca46 Improve the post-hiders and the header alerts (#2074)
* Improve the post-hider and the header alerts

* Adjust the contenthider (icon, font size)
2023-12-04 12:51:08 -08:00
Paul Frazee 971c402788 Add PWI build flag (#2082) 2023-12-04 12:45:40 -08:00
dan a687d3f028 Allow scroll by dragging profile header (#2068) 2023-12-04 12:03:32 -08:00
Paul Frazee 9562a80908 Allow posts with just an external link (#2079) 2023-12-04 11:59:59 -08:00
Paul Frazee e47c3dbe0e Tweak the loading placeholders to use the full space and position better (#2072) 2023-12-04 11:59:27 -08:00
Paul Frazee a01497fed4 Hackfix to composer focus on web (#2057) 2023-12-03 18:45:25 -08:00
Paul Frazee 0ab19b45a2 Tweak layout a bit more 2023-12-02 17:56:04 -08:00
Paul Frazee 07afcab6b2 Merge branch 'fix-jump-in-suggested-follows-tabbar' of https://github.com/tarikfp/social-app into uiwork-reflow-issue 2023-12-02 17:52:21 -08:00
dan 2698a6b50b Fix show suggested button color (#2069) 2023-12-01 16:23:00 -08:00
dan bb22ebd588 Fix scroll jump on Back (#2067) 2023-12-01 14:05:10 -08:00
Eric Bailey 9dec9d7276 Only poll feed when feed page is focused (#2064)
* Do no poll when screen is not focused

* Avoid polling unless focused

* Handle homepage in background

* Fix the intl:check to ignore comments in diffs

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-12-01 13:40:50 -08:00
Cooper Edmunds 9a13fe50cc Use lastActiveTab from AsyncStorage for non-desktop 2023-12-01 15:18:25 -05:00
Eric Bailey f80bd30ef4 Better handle feed auth failures (#2060)
* Better handle feed auth failures

* Add comment
2023-12-01 09:29:50 -08:00
Eric Bailey edc9ac272c Add Lingui check to CI (#2058)
* Install lingui eslint plugin, ignore compiled files

* Add lingui step to Lint workflow

* Run check in other job

* Add compile step to web build

* Temp: add a new string to test with

* Add message

* Revert "Temp: add a new string to test with"

This reverts commit ef5459d00c.

* Un-track compiled files

* Add check scripts to package.json, build step

* Make sure to compile translations prior to test/typecheck

* Rename scripts
2023-12-01 09:14:10 -08:00
Paul Frazee b05cf9f3c1 Bump ios build number 2023-11-30 18:57:18 -08:00
Paul Frazee f8c46c08ca Improve unread notif tracking (#2056) 2023-11-30 18:56:55 -08:00
Paul Frazee 826cbbd4bf Maintain some feed data to avoid needless glimmers (#2054) 2023-11-30 18:49:23 -08:00
dan 9fa90bb8d9 Optimize pager rendering (#2055)
* Pull out and memoize PagerTabBar

* Avoid invalidating onScroll and add throttling

* Make isScrolledDown update non-blocking

* Fix types
2023-12-01 02:11:05 +00:00
Paul Frazee 23ad3ad98b Various fixes (#2053)
* Fix invite code translations (plural api not availabe in ios)

* Speed up the composer animation a smidge
2023-11-30 17:03:38 -08:00
Cλctys 5839d2a30c Fix scuffed web styles caused by overlapping viewport breakpoint boundaries (#1985)
* fixed lack of styles on 1300px web viewport width by adjusting tablet breakpoints

* fixed lack of styles on 800px web viewport width by adjusting mobile breakpoints

* changed `maxWidth` values in viewports to `n - 1` format
2023-11-30 16:14:36 -08:00
Paul Frazee 85b2b390df Merge branch 'mozzius-invite-code-warning' into main 2023-11-30 16:13:23 -08:00
Paul Frazee 1755c2e538 Merge branch 'invite-code-warning' of https://github.com/mozzius/bluesky-official-app into mozzius-invite-code-warning 2023-11-30 16:13:16 -08:00
Paul Frazee dcbceb5d9f Merge branch 'mozzius-copy-link' into main 2023-11-30 16:06:07 -08:00
Paul Frazee 21af10bc3d Merge branch 'copy-link' of https://github.com/mozzius/bluesky-official-app into mozzius-copy-link 2023-11-30 16:05:34 -08:00
Samuel Newman 9375532698 Add enter/exit animations to modals on web (#2042)
* add reanimated layout animations to modals

* reorder imports

* pass context to scroll handler patches
2023-11-30 16:02:46 -08:00
Eric Bailey abe61b7517 Disable OTA for now (#2052) 2023-11-30 15:48:07 -08:00
Eric Bailey 28f8999252 Beep boop (#2051) 2023-11-30 13:53:34 -08:00
Eric Bailey 76a3c66f54 Eric/profile feeds list (#2049)
* Fix profile feedgens view

* Fix profile lists view

* Translate
2023-11-30 13:40:49 -08:00
Eric Bailey 47b0d36b73 Hide normal mute if muted by list, and invalidate profile query upon list mute (#2048)
* Hide normal mute if muted by list, and invalidate profile query upon list mute

* Prevent profile flashing
2023-11-30 13:40:01 -08:00
dan 46b63accb8 Rewrite the shadow logic to look inside the cache (#2045)
* Reset

* Associate shadows with the cache

* Use colocated helpers

* Fix types

* Reorder for clarity

* More types

* Copy paste logic for profile

* Hook up profile query

* Hook up suggested follows

* Hook up other profile things

* Fix shape

* Pass setShadow into the effect deps

* Include reply posts in the shadow cache search

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-11-30 13:35:58 -08:00
Paul Frazee 143fc80951 Merge branch 'main' of github.com:bluesky-social/social-app into main 2023-11-30 12:47:48 -08:00
Paul Frazee ee82e78d5c Fix missing export in e2e version 2023-11-30 12:47:40 -08:00
dan cb193a827d Fix lag when switching tabs (#2046) 2023-11-30 19:57:54 +00:00
Samuel Newman b7599e5bea generate messages 2023-11-30 17:12:21 +00:00
Samuel Newman b164f151cc Merge remote-tracking branch 'upstream/main' into invite-code-warning 2023-11-30 17:11:51 +00:00
Samuel Newman ee5d7a63ce update messages 2023-11-30 17:09:59 +00:00
Samuel Newman c06611fb71 show informative message when token scope is wrong 2023-11-30 17:09:10 +00:00
Samuel Newman 5c78b2266f add "copy link" text 2023-11-30 16:40:19 +00:00
Paul Frazee fcd22d4ccb Adjust stale-caches and dont group read&unread notifs together (#2041)
* Dont group read & unread notifications together

* Remove and reduce some stale cache times

* Keep the staleTime on the post-feed

* Bring back the load-bearing staletime on profile
2023-11-29 20:27:39 -08:00
Paul Frazee c3e97fee1d Bump ios build number 2023-11-29 20:12:55 -08:00
Paul Frazee a03f57c8c3 Apply feed preferences (react-query refactor) (#2040)
* Actually implement the feed tuners hook

* Move feed-tuner pass into select() to have it apply immediately on change
2023-11-29 20:11:01 -08:00
Eric Bailey 3e1b2346ee Close modals if switching to unauthed account (#2039) 2023-11-29 19:28:18 -08:00
Eric Bailey b83fa95793 Handle nsfw feeds in public view (#2035)
* Handle nsfw feeds in public view

* Translate

* Refactor, new approach

* Translate

* Copy

* Translate

* Revert changes to Feed

* Remove unused retry

* Translate
2023-11-29 19:27:58 -08:00
Eric Bailey 005fc7cc38 Disable retrying globally (#2038) 2023-11-29 17:50:14 -08:00
Paul Frazee c07d27645f Merge branch 'CooperEdmunds-2011-link-in-feeds-tab-bar' into main 2023-11-29 17:49:46 -08:00
Paul Frazee 1140b05b83 Slightly less verbose variable 2023-11-29 17:37:15 -08:00
Paul Frazee 4c7b9652db Merge branch '2011-link-in-feeds-tab-bar' of https://github.com/CooperEdmunds/social-app into CooperEdmunds-2011-link-in-feeds-tab-bar 2023-11-29 17:35:57 -08:00
Paul Frazee dc53ef1ec1 Merge branch 'CooperEdmunds-2011-default-pin-feed-on-add' into main 2023-11-29 17:25:36 -08:00
Paul Frazee 6e30c339da Merge branch '2011-default-pin-feed-on-add' of https://github.com/CooperEdmunds/social-app into CooperEdmunds-2011-default-pin-feed-on-add 2023-11-29 17:25:29 -08:00
Paul Frazee 630637874d Fix state lifecycle management with post-feed query, solving the duplicate key issue (#2034)
* Assign keys to feed slices via a counter, to enable duplicate items in the feed if needed

* Move post-feed query state into the query's page params to consistently bind their lifecycles
2023-11-29 18:58:14 -06:00
Cooper Edmunds 3fbac466ac Update home-screen tests related to feeds 2023-11-29 19:28:35 -05:00
Eric Bailey a59d235e8b Improve feed reordering with optimistic updates (#2032)
* Optimisticaly update order of saved feeds

* Better show disabled state for pin button

* Improve loading/disabled states

* Improve placeholder

* Simplify loading components
2023-11-29 16:17:27 -08:00
Eric Bailey 3ca4bd805a Re-enable fetch monkey-patch (#2036)
* Re-enable fetch monkey-patch

* Reorder
2023-11-29 16:00:13 -08:00
Cooper Edmunds 34759798eb Stop adding whats-hot for new users 2023-11-29 15:24:14 -05:00
Cooper Edmunds 8ceabe2a11 Conditionally render feeds link in feeds tab bar 2023-11-29 14:49:18 -05:00
Cooper Edmunds ed391c346d Add hasPinnedCustomFeedOrList to usePinnedFeedsInfos hook 2023-11-29 14:23:19 -05:00
Paul Frazee 4b3ec54add Fix infinite query reloading behavior (reset, not invalidate) (#2031)
* Reset, not invalidate, notification queries

* Reset, not invalidate, feed queries
2023-11-29 11:15:35 -08:00
Paul Frazee 9239efac9c Refactor the notifications to cache and reuse results from the unread-notifs checks (#2017)
* Refactor the notifications to cache and reuse results from the unread-notifs checks

* Fix types
2023-11-29 10:20:14 -08:00
Ansh 620e002841 Show logged out view when adding accounts (#2020)
* show logged out view when adding accounts

* Handle existing signed-in account

* Show which account is currently logged in

* Fix showing toasts

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2023-11-29 10:11:06 -08:00
Paul Frazee 6fe2b52f68 Get more rigorous about getAgent() consistency (#2026)
* Get more rigorous about getAgent() consistency

* Update the feed wrapper API to use getAgent() directly
2023-11-29 10:10:04 -08:00
Cooper Edmunds ca357ecbcf Add pinOnSave and use it in discover feeds list 2023-11-29 09:05:26 -05:00
dan 9fb2c29c67 Disable RQ structural sharing (#2022) 2023-11-28 20:48:08 -08:00
Eric Bailey 6f7032d42b Full send Sentry (#2018)
* Update build profiles, sentry config

* Enable sentry

* Ok actually enable in dev

* Remove debug

* Add TF build

* Fix typo

* Remove debug

* Remove unecessary config

* Fix typo

* Set env in Expo

* Remove scripts

* Clarify

* Replace invalid character

* Align on release/dist

* Add build version

* Just use package version

* Align dist
2023-11-28 21:49:37 -06:00
dan b778017000 Fix memory leak on mobile web tab navigation (#2021)
* Add navigationAction prop to Link

* Bottom bar should use navigate() action
2023-11-28 22:48:00 +00:00
Eric Bailey bb9d340d42 Fix pinned feeds mutation issue (#2016) 2023-11-28 12:50:41 -06:00
Eric Bailey 0b2c85b967 Fix pinned feeds re-ordering (#2014) 2023-11-28 09:04:15 -08:00
Eric Bailey bdabfa9d38 Guard against following tab stub being used un-authed (#2013) 2023-11-28 08:44:25 -08:00
Eric Bailey cf1b0b1881 Handle other feed auth response (#2012) 2023-11-28 08:43:25 -08:00
Paul Frazee 60886b76c8 Bump ios build number 2023-11-27 17:46:13 -08:00
Eric Bailey 17a183514a Ensure we use following feed if merge is disabled (#2005) 2023-11-27 17:42:02 -08:00
Paul Frazee f580d4daf0 Restore post-thread caching behaviors (react-query refactor) (#2010)
* Rework resolve-did and resolve-uri queries to be smarter about cache reuse

* Precache handle resolutions

* Remove old unused code

* Load placeholder threads from the post-feed and notifications-feed queries

* Remove logs

* Fix bad ref

* Add loading spinners to the cache-loading thread view

* Scroll replies into view when loading threads

* Add caching within a thread

* Fix: dont show bottom border when the child spinner is active
2023-11-27 17:41:30 -08:00
Eric Bailey d4714baf13 [PWI] Home (#2007)
* Fix up home page

* Fix spacing

* Remove unused default tab

* Update logic
2023-11-27 17:17:09 -08:00
Eric Bailey a741a201b9 Don't check unread if session doesn't exist (#2008) 2023-11-27 16:40:20 -08:00
Eric Bailey 26bbe07068 Add more debugging to auth (#2009) 2023-11-27 16:39:53 -08:00
Eric Bailey 5fd2d36273 Filter out blocked posts from thread view (#2006) 2023-11-27 12:41:53 -08:00
Paul Frazee cd43adf698 Two fixes to react-key generation (#2004)
* Fix missing react keys in ListMembers

* Fix react key construction for the posts feed
2023-11-27 12:30:41 -08:00
Eric Bailey 1dc017367e Make sure logger serializes errors (#2001) 2023-11-27 10:28:21 -08:00
Eric Bailey 6758755316 Nav home after switch accounts (#2002) 2023-11-27 10:27:38 -08:00
Eric Bailey 828e53d533 Fix soft reset scroll on profile (#1998) 2023-11-27 09:43:05 -06:00
Ansh 0079030244 create build v1.57 (#2000) 2023-11-26 13:46:31 -08:00
Eric Bailey 08355e5166 Raise levels of some logs during pre-release (#1994) 2023-11-26 13:44:30 -08:00
Eric Bailey 4a59227459 Fix translation strings (#1995) 2023-11-26 13:44:17 -08:00
Eric Bailey 3007c1dc8a Fix bad references in account switcher (#1999) 2023-11-26 13:44:08 -08:00
Eric Bailey 486fb72798 Fix double border, improve load state (#1997) 2023-11-26 13:43:49 -08:00
Eric Bailey e9a11114d3 [PWI] Clarify different ways of clearing current account/logout (#1991)
* Clarify different ways of clearing current account/logout

* Reorder log
2023-11-24 17:41:42 -06:00
Eric Bailey 20b699a008 Fall back for non-public feeds (#1988) 2023-11-24 17:37:28 -06:00
Eric Bailey 1bcbc0cf2a Fix onboarding web layout (#1990) 2023-11-24 16:40:07 -06:00
dan f2d164ec23 PWI: Refactor Shell (#1989)
* Vendor createNativeStackNavigator for further tweaks

* Completely disable withAuthRequired

* Render LoggedOut for protected routes

* Move web shell into the navigator

* Simplify the logic

* Add login modal

* Delete withAuthRequired

* Reset app state on session change

* Move TS suppression
2023-11-24 22:31:33 +00:00
Ansh 4b59a21cac Allow dynamic locale imports for web (#1971)
* allow dynamic locale imports for web

* remove unnecessary file
2023-11-24 12:35:25 -08:00
dan 43c8fb6f03 Fix missing effect cleanup (#1986) 2023-11-24 19:22:00 +00:00
dan 7ccbd14d5f Show feeds in PWI (#1984) 2023-11-23 19:43:23 +00:00
dan 4272d291a9 PWI: Profile (#1982)
* PWI: Profile

* Show replies conditionally

* Dismiss modals on auth action
2023-11-23 00:30:49 +00:00
Eric Bailey edf3114e47 Fixes 1731, compare URLs case-insensitive (#1980) 2023-11-22 17:20:35 -06:00
Eric Bailey ec819f06ce [PWI] Clean up logged out screen (#1978)
* Clean up logged out screen

* Better responsive styles
2023-11-22 16:53:25 -06:00
Eric Bailey 95f8bd8ddb [PWI] Misc (#1977)
* PostDropdownBtn

* Hide reply prompt

* Hide FABs
2023-11-22 16:53:11 -06:00
dan 08333002cc Implement "scroll to top" for profile tabs (#1973)
* Hook up scroll to top handlers

* Scroll and invalidate Feeds/Lists

* Fix index calc due to conditional tabs

* Reorder lines for clarity
2023-11-22 04:25:11 +00:00
Eric Bailey 3de1d556a9 [PWI] Feeds (#1970)
* Feeds page

* Hide save buttons on Feeds page

* ProfileFeed
2023-11-21 17:36:29 -06:00
Eric Bailey 8da10a5edc Search page (#1969) 2023-11-21 17:35:53 -06:00
Eric Bailey 47d2d3cbf2 [PWI] Shell (#1967)
* Sidebars

* Bottom bar

* Drawer

* Translate

* Spacing fix

* Fix responsive regression

* Fix nit
2023-11-21 16:58:13 -06:00
dan 4c4ba553bd Shadow refactoring and improvements (#1959)
* Make shadow a type-only concept

* Prevent unnecessary init state recalc

* Use derived state instead of effects

* Batch emitter updates

* Use object first seen time instead of dataUpdatedAt

* Stop threading dataUpdatedAt through

* Use same value consistently
2023-11-21 22:42:30 +00:00
Eric Bailey f18b9b32b0 PWI Base (#1964)
* Base work for public view

* Make default moderation settings more restrictive

* Fix type

* Handle showing sign-in on authed actions

* Fix hoc logic

* Simplify prefs logic

* Remove duplicate method

* Add todo

* Clean up RepostButton.web

* Fix x button color

* Add todo

* Retain existing label prefs for now, use separate logged out settings

* Clean up useAuthedMethod, rename to useRequireAuth

* Add todos

* Move dismiss logic to withAuthRequired

* Ooops add web

* Block public view in prod

* Add todo

* Fix bad import
2023-11-21 10:57:34 -06:00
Eric Bailey 71b59021b9 Add dev-only button to clear storage (#1965)
* Add dev-only button to clear storage

* Add legacy storage clearing too

* Use try/catch
2023-11-21 10:16:22 -06:00
Eric Bailey 3a21c02b8a Re-wrap forgot pw success in layout (#1966) 2023-11-21 10:04:23 -06:00
Ansh c5b6f88e9a Hindi Internationalization (#1914)
* get basic hindi support to work

* get web app language switcher in

* Refactor i18n implementation and remove unused
code

* add missing strings

* add dropdowns and modals missing strings

* complete all hindi translations

* fix merge conflicts

* fix legeacy persisted state

* fix data in RecommendedFeeds

* fix lint
2023-11-20 13:29:27 -08:00
Eric Bailey 019aae5f01 Improve dedupe logic on search suggestions (#1958) 2023-11-17 12:15:14 -08:00
dan 7c51a3931a Optimize notifications rendering (#1957) 2023-11-17 17:51:44 +00:00
dan d1c14940fb Add missing type (#1955) 2023-11-17 09:47:36 -08:00
Eric Bailey 0dfc039a47 Fix some key errors (#1951) 2023-11-17 11:34:27 -06:00
dan c858b58307 Do less work (#1953) 2023-11-17 09:31:26 -08:00
Eric Bailey 9c8a1b8a31 Fix some type errors (#1952)
* Fix some low-hanging type errors

* Override scrollRef types on profile
2023-11-17 11:08:45 -06:00
dan 6c8f043e37 Freeze inactive screens (#1949) 2023-11-16 22:20:08 -08:00
dan a8aed2c38e Use non-reactive callbacks for some methods (#1948)
* Use non-reactive callbacks for some methods

* Remove unnecessary state

* Use non-reactive callbacks for modals
2023-11-16 22:18:37 -08:00
dan a10adf8908 Add some memoization (#1947) 2023-11-16 18:52:41 -08:00
Paul Frazee e358c3cc30 Improve notification load behaviors (#1943)
* Dont use the stale cache for notifs-feed

* Add a delay to marking all read to avoid marking upcoming posts as read

* Trigger automatic notifications refresh when navigating to the tab, in certain conditions
2023-11-16 18:27:05 -08:00
Paul Frazee 357c752a21 Move the current agent to a global and reset RQ queries on agent change (#1946) 2023-11-16 18:26:22 -08:00
dan 3043b32468 Fix jumpy modal regression (#1945) 2023-11-16 18:01:51 -08:00
Paul Frazee c03c744008 Notifs refresh improvement (#1941)
* Show loading state in notifications

* Fix invalidation of notifs

* Move spinner below header

* Fix: dont show spinner on initial load
2023-11-16 15:50:57 -08:00
Eric Bailey 6f57192bd5 Revise cache times (#1942)
(cherry picked from commit 1be2060a2b)
2023-11-16 15:43:53 -08:00
Paul Frazee 1952705ded Disable refetchOnWindowFocus on react-query (#1936) 2023-11-16 15:42:37 -08:00
Eric Bailey 6dfb2a232f Caching heuristics (#1938)
* Tempfix profile load

* First pass at staleTime
2023-11-16 15:28:50 -08:00
dan f89dc63801 Add profile loading shim (#1940) 2023-11-16 23:23:58 +00:00
dan 22df70b3cc Fix blank feed (#1935)
* Split feed into two components

* Rm unused

* Add a spinner
2023-11-16 14:37:48 -08:00
dan 8475312422 Add a mutation queue to fix race conditions in toggles (#1933)
* Prototype a queue

* Track both current and pending actions

* Skip unnecessary actions

* Commit last confirmed state to shadow

* Thread state through actions over time

* Fix the logic to skip redundant mutations

* Track status

* Extract an abstraction

* Fix standalone mutations

* Add types

* Move to another file

* Return stable function

* Clean up

* Use queue for muting

* Use queue for blocking

* Convert other follow buttons

* Don't export non-queue mutations

* Properly handle canceled tasks

* Fix copy paste
2023-11-16 14:01:01 -08:00
Paul Frazee 54faa7e176 Remove deprecated models and mobx usage (react-query refactor) (#1934)
* Update login page to use service query

* Update modal to use session instead of store

* Move image sizes cache off store

* Update settings to no longer use store

* Update link-meta fetch to use agent instead of rootstore

* Remove deprecated resolveName()

* Delete deprecated link-metas cache

* Delete deprecated posts cache

* Delete all remaining mobx models, including the root store

* Strip out unused mobx observer wrappers
2023-11-16 12:53:43 -08:00
Paul Frazee e637798e05 Refactor account-creation to use react-query and a reducer (react-query refactor) (#1931)
* Refactor account-creation to use react-query and a reducer

* Add translations

* Missing translate
2023-11-16 11:16:31 -08:00
Paul Frazee 9f7a162a96 Refactor app passwords to use react-query (#1932) 2023-11-16 11:11:56 -08:00
Eric Bailey 310a7eaca7 Some me model cleanup (#1928)
* Replace me model in post dropdown btn

* Replace delete account logic

* Replace me model in bottom bar web

* Replace me model in bottom bar

* Replace me model in reply prompt

* Better fallback

* Fix reference

* Fix bad ref in bottom bar
2023-11-16 10:54:03 -08:00
Eric Bailey a652b52b88 Refactor ChangeHandle modal (#1929)
* Refactor ChangeHandle to use new methods

* Better telemetry

* Remove unused logic

* Remove caching

* Add error message

* Persist service changes, don't fall back on change handle
2023-11-16 09:16:16 -08:00
Eric Bailey e6efeea7c0 Refactor invites modal (#1930)
* Refactor invites modal

* Replace in drawer

* Delete stuff from me model
2023-11-16 08:40:31 -08:00
Paul Frazee 8a1fd160e6 Update UserInfoText component (#1927) 2023-11-16 08:19:08 -08:00
Paul Frazee a84b2f9f2f Close active elems (react-query refactor) (#1926)
* Refactor closeAny and closeAllActiveElements

* Add close lightbox

* Switch to hooks

* Fixes
2023-11-16 08:18:59 -08:00
Eric Bailey 0de8d40981 Misc cleanup (#1925)
* Remove unused prefs

* Cleanup

* Remove my-follows cache

* Replace moderationOpts in ProfileCard comp

* Replace moderationOpts in FeedSlice

* Remove preferences model
2023-11-15 18:35:58 -08:00
Paul Frazee e749f2f3a5 Factor lightbox out into hook/context (#1919) 2023-11-15 18:17:03 -08:00
Paul Frazee 03b20c70e4 Fix memo params causing tuner to lose state (#1924) 2023-11-15 17:42:15 -08:00
Eric Bailey 952f5033d0 Fixes analytics init on web (#1923) 2023-11-15 17:37:55 -08:00
Eric Bailey 610eeecd26 Fix onboarding follows (#1922) 2023-11-15 17:37:32 -08:00
Eric Bailey 8857ba70c6 Improve types (#1921) 2023-11-15 17:23:59 -08:00
Eric Bailey d8b26edb56 Remove post thread (#1920)
* Delete post-thread model

* Remove post-thread-item

* Remove unused types
2023-11-15 17:18:45 -08:00
Paul Frazee 6616b2bff0 Shell behaviors update (react-query refactor) (#1915)
* Move tick-every-minute into a hook/context

* Move soft-reset event out of the shell model

* Update soft-reset listener on new search page

* Implement session-loaded and session-dropped events

* Update analytics and push-notifications to use new session system
2023-11-15 17:17:50 -08:00
Eric Bailey f23e9978d8 Update post embed fetching to use new methods (#1916)
* Update post embed fetching to use new methods

* Use session agent
2023-11-15 17:09:13 -08:00
Eric Bailey 9bcd00b831 No need to await follows anymore, fix cold load search (#1918) 2023-11-15 17:08:08 -08:00
Eric Bailey fe1a7183fc Translate some things on search (#1917) 2023-11-15 19:00:43 -06:00
Eric Bailey 22b76423a0 Search page (#1912)
* Desktop web work

* Mobile search

* Dedupe suggestions

* Clean up and reorg

* Cleanup

* Cleanup

* Use Pager

* Delete unused code

* Fix conflicts

* Remove search ui model

* Soft reset

* Fix scrollable results, remove observer

* Use correct ScrollView

* Clean up layout

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-11-15 15:55:28 -08:00
Paul Frazee d5ea31920c Autocomplete updates (react-query refactor) (#1911)
* Unify the autocomplete code; drop fuse

* Persist autocomplete results while they're in progress

* Commit lockfile

* Use ReturnType helper

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2023-11-15 14:39:22 -08:00
dan 839e8e8d0a Post PostLikedBy and PostRepostedBy to RQ (#1913)
* Port PostRepostedBy to RQ

* Port PostLikedBy to RQ
2023-11-15 14:04:25 -08:00
dan e699df21c6 Port Profile Followers/Follows to RQ (#1893)
* Port user followers to RQ

* Port user follows to RQ

* Start porting FollowButton to RQ

* Fix RQ key

* Check pending

* Fix shadow and pending states

* Rm unused

* Remove last usage of useFollowProfile
2023-11-14 17:55:54 -08:00
Eric Bailey d1cb74febe Desktop user autocomplete search (#1906)
* Fix notification provider order, add comments

* Remove log

* Add actor typeahead handling

* Trim down desktop search styles and hooks

* Clean up moderation
2023-11-14 17:51:23 -08:00
Eric Bailey ab1ce078ec Fix openCamera, openCropped, lightbox model, and image model usages (#1908) 2023-11-14 17:46:46 -08:00
Eric Bailey 7d5e01f433 Fix profile list (#1907) 2023-11-14 17:45:38 -08:00
Paul Frazee e08d06f263 Fix image order in 4-image post embeds (close #1895) (#1904) 2023-11-14 17:10:58 -06:00
Eric Bailey 68767d597e Fix notification provider order, add comments (#1905)
* Fix notification provider order, add comments

* Remove log
2023-11-14 16:54:02 -06:00
Eric Bailey ab6e3f2c5d Fix self mention, resolve handle (#1903)
* Fix self mention, resolve handle

* Use queryClient

* Fix type

* Remove staleTime
2023-11-14 14:16:56 -06:00
Paul Frazee 8e4a3ad5b6 Add Shadow type (#1900) 2023-11-14 12:10:39 -08:00
Eric Bailey 00f8c8b06d Fix root sibling context issue (#1902) 2023-11-14 10:46:37 -08:00
Paul Frazee 0a26e78dcb Composer update (react-query refactor) (#1899)
* Move composer state to a context

* Rework composer to use RQ

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2023-11-14 10:41:55 -08:00
Paul Frazee c687172de9 Fix poll latest loop (#1901)
* Avoid pollLatest loop

* Set poll interval on profile feeds
2023-11-14 12:33:05 -06:00
Eric Bailey 3fde1bea1b Handle end of feed (#1898)
* Handle end of feed

* Add isFetching
2023-11-14 11:28:40 -06:00
Eric Bailey 4355f0fd9a Refactor onboarding suggested follows (#1897)
* Refactor onboarding suggested follows

* Fix error state, track call

* Remove todo

* Use flatmap

* Add additional try catch

* Remove todo
2023-11-14 11:25:37 -06:00
Paul Frazee a81c4b68fa Update Muted and Blocked accounts screens (react-query refactor) (#1892)
* Add my-blocked-accounts and my-muted-accounts queries

* Update ProfileCard to use the profile shadow cache and useModerationOpts

* Update blocked accounts and muted accounts screens
2023-11-13 17:30:56 -08:00
Paul Frazee 0501c2be77 Profile cleanup (react-query refactor) (#1891)
* Only fetch profile tab content when focused

* Fix keys

* Add missing behaviors to post tabs

* Delete old profile mobx model
2023-11-13 15:12:41 -08:00
Paul Frazee 47204d9551 Update profile preview to use react-query (#1890) 2023-11-13 14:57:53 -08:00
Paul Frazee 9fca7b3af6 Add feedgens tab to profile (#1889) 2023-11-13 14:46:19 -08:00
Eric Bailey b04748e703 Factor out store from post-feed query (#1888) 2023-11-13 14:07:18 -08:00
Eric Bailey 06eb8b9a4c Factor our feed source model (#1887)
* Refactor first onboarding step

* Replace old FeedSourceCard

* Clean up CustomFeedEmbed

* Remove discover feeds model

* Refactor ProfileFeed screen

* Remove useCustomFeed

* Delete some unused models

* Rip out more prefs

* Factor out treeView from thread comp

* Improve last commit
2023-11-13 13:53:57 -08:00
Paul Frazee a01463788d More profile refactor updates (#1886)
* Update the profile avatar lightbox

* Update profile editor

* Add dynamic likes tab

* Add dynamic feeds and lists tabs

* Implement lists listing on profiles
2023-11-13 13:29:33 -08:00
dan 8217761363 Fix jump for web pager (#1884) 2023-11-13 13:03:47 -08:00
dan bb4ed3cd49 Fix lint on main (#1885) 2023-11-13 13:01:26 -08:00
dan e1938931e0 Refactor profile screen to use new pager and react-query (#1870)
* Profile tabs WIP

* Refactor the profile screen to use react-query (WIP)

* Add the profile shadow and get follow, mute, and block working

* Cleanup

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-11-13 10:35:15 -08:00
Eric Bailey c3edde8ac6 More prefs cleanup (#1882)
* Update useHomeTabs

* Use queryClient to fetch

* Replace useHomeTabs and useDesktopRightNavItems

* Fix type error

* Some cleanup
2023-11-13 10:21:53 -08:00
Eric Bailey 37a2204483 Update clear account behavior, ensure agent is set (#1881) 2023-11-13 08:20:38 -08:00
Paul Frazee b445c15cc9 Refactor notifications to use react-query (#1878)
* Move broadcast channel to lib

* Refactor view/com/post/Post and remove temporary 2 components

* Add useModerationOpts hook

* Refactor notifications to use react-query

* Fix: only trigger updates in useModerationOpts when the values have changed

* Implement unread notification tracking

* Add moderation filtering to notifications

* Handle native/push notifications

* Remove dead code

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2023-11-12 18:13:11 -08:00
Eric Bailey c584a3378d Refactor My Feeds (#1877)
* Refactor My Feeds screen

* Remove unused feed UI models

* Add back PTR
2023-11-12 16:26:02 -08:00
Paul Frazee d9e0a927c1 Refactor lists to use new queries (#1875)
* Refactor lists queries to react-query

* Delete old lists-list model

* Implement list, list-members, and list-memberships react-queries

* Update CreateOrEditList modal

* First pass at my-follows and actor-autocomplete queries

* Update ListAddUserModal to use new queries, change to ListAddRemoveUsersModal

* Update UserAddRemoveLists modal

* Remove old TODO

* Fix indent, autocomplete query

* Add a todo

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2023-11-12 12:45:25 -08:00
Eric Bailey 05b728fffc Eric/preferences (#1873)
* Add initial preferences query, couple mutations

* Remove unused

* Clean up labels, migrate getModerationOpts

* Add birth date handling

* Migrate feed prefs

* Migrate thread view prefs

* Migrate homeFeed to use existing key name

* Fix up saved feeds in response, no impl yet

* Migrate saved feeds to new hooks

* Clean up more of preferences

* Fix PreferencesThreads load state

* Fix modal dismissal

* Small spacing fix

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-11-12 11:31:11 -08:00
Paul Frazee c8c308e31e Refactor feeds to use react-query (#1862)
* Update to react-query v5

* Introduce post-feed react query

* Add feed refresh behaviors

* Only fetch feeds of visible pages

* Implement polling for latest on feeds

* Add moderation filtering to slices

* Handle block errors

* Update feed error messages

* Remove old models

* Replace simple-feed option with disable-tuner option

* Add missing useMemo

* Implement the mergefeed and fixes to polling

* Correctly handle failed load more state

* Improve error and empty state behaviors

* Clearer naming
2023-11-10 15:34:25 -08:00
dan 51f04b9620 Use min height for pager lists and increase it (#1869) 2023-11-10 22:29:12 +00:00
dan 86b4842d67 Fix initial pager gap after fast scroll (#1868) 2023-11-10 21:23:17 +00:00
dan 91f8a23fbc Scroll sync in the pager without jumps (#1863) 2023-11-10 19:54:33 +00:00
dan 65def37165 Push useAnimatedScrollHandler down everywhere to work around bugs (#1866)
* Move useOnMainScroll handlers to leaves

* Force Feed to always take handlers

* Pass handlers from the pager
2023-11-10 19:00:46 +00:00
Eric Bailey e0e5bc8fd8 Fix type errors (#1867) 2023-11-10 18:20:33 +00:00
Eric Bailey 6513055d02 Merge pull request #1860 from bluesky-social/eric/startup
Web login/signup and shell
2023-11-10 11:31:36 -06:00
Eric Bailey 436a14eabb Pare down session as much as possible 2023-11-10 09:59:04 -06:00
Eric Bailey d0d93168d4 Clean up isDirty handling 2023-11-10 08:59:39 -06:00
dan 8d7475c130 Work around web stale closure bug in Reanimated (#1865) 2023-11-10 14:58:13 +00:00
Eric Bailey 499021229a Remove error states, just best effort 2023-11-10 08:53:23 -06:00
Eric Bailey 60386f8f07 Swap order 2023-11-10 08:48:15 -06:00
Eric Bailey b0c9cce5c3 Follow conventions for query, use isDirty flag in session store to avoid unneccessary writes 2023-11-10 08:46:45 -06:00
Eric Bailey 742f53d1ec Comments 2023-11-09 20:46:45 -06:00
Eric Bailey 2d7b89c6a1 Cleanup getProfile 2023-11-09 20:39:54 -06:00
Eric Bailey ab878ba9a6 Web login/signup and shell 2023-11-09 20:35:17 -06:00
dan 487d871cfd Pull useAnimatedScrollHandler back up (#1858)
* Revert "Pull animated scroll handler down from pager"

This reverts commit ecebb78e40.

* Only handle onScroll for current page
2023-11-09 23:47:54 +00:00
Paul Frazee fb4f5709c4 Refactor post threads to use react query (#1851)
* Add post and post-thread queries

* Update PostThread components to use new queries

* Move from normalized cache to shadow cache model

* Merge post shadow into the post automatically

* Remove dead code

* Remove old temporary session

* Fix: set agent on session creation

* Temporarily double-login

* Handle post-thread uri resolution errors
2023-11-09 15:35:25 -08:00
Eric Bailey 625cbc435f First pass at a session handler (#1850)
* First pass at a session handler

* TODOs

* Fix recursion

* Couple more things

* Add back resume session concept

* Handle ready

* Cleanup of initial loading states

* Handle init failure

* Cleanup

* Remove account

* Add updateCurrentAccount

* Remove log

* Cleanup

* Integrate removeAccount

* Add hasSession

* Add to App.native, harden migration

* Use effect to persist data
2023-11-09 15:14:51 -08:00
dan 664e7a91a9 Fix tab alignment on the web (#1857) 2023-11-09 21:56:25 +00:00
dan c627a766cd Show tabs when swiping feeds (#1856) 2023-11-09 21:45:31 +00:00
dan 7a55ca6133 Sync top/bottom bar disappearance to the scroll (#1855)
* Disable existing code that toggles shell

* Make shell mode a float

* Translate based on the gesture

* Track header and footer heights

* Add web support

* Fix types and cleanup

* Add back isScrolled logic

* Add comments
2023-11-09 12:15:05 -08:00
Ansh 1dcf882619 Hotfix internationalization on mobile (#1854)
* hotfix internationalization on mobile

* remove comment
2023-11-09 10:49:46 -08:00
Ansh 4c7850f8c4 Internationalization & localization (#1822)
* install and setup lingui

* setup dynamic locale activation and async loading

* first pass of automated replacement of text messages

* add some more documentaton

* fix nits

* add `es` and `hi`locales for testing purposes

* make accessibilityLabel localized

* compile and extract new messages

* fix merge conflicts

* fix eslint warning

* change instructions from sending email to opening PR

* fix comments
2023-11-09 10:04:16 -08:00
dan 82059b7ee1 Hide/show header and footer without re-renders, take two (#1849)
* Remove callsites using the state value

* Remove unused code

* Change shell mode without re-renders

* Adjust "write your reply" for mode
2023-11-09 00:25:27 +00:00
Eric Bailey bd531f2344 Pull language methods into api context (#1847)
* Pull language methods into api context

* Rename for consistency
2023-11-08 11:55:38 -08:00
Eric Bailey f18b15241a Add modal state provider, replace usage except methods (#1833)
* Add modal state provider, replace usage except methods

* Replace easy spots

* Fix sticky spots

* Replace final usages

* Memorize context objects

* Add more warnings
2023-11-08 10:34:10 -08:00
Paul Frazee 5eadadffbf State layer cleanup: move alt-text-required into preferences and fix a bug in reminders (#1845)
* Move alt-text-required into preferences

* Fix bug: reminders now using new onboarding state
2023-11-08 09:46:07 -08:00
Paul Frazee 5843e212c0 Move language preferences to new persistence + context (#1837) 2023-11-08 09:38:28 -08:00
Paul Frazee e75b2d508b Move invite-state to new persistence + context and replace the notifications with just showing uses in the modal (#1840) 2023-11-08 09:10:59 -08:00
Paul Frazee 74f8390f1d Move muted threads to new persistence + context (#1838) 2023-11-08 09:08:42 -08:00
Paul Frazee 4afed4be28 Move onboarding state to new persistence + reducer context (#1835) 2023-11-08 09:04:06 -08:00
Paul Frazee 3a211017d3 Move require alt-text to new persistence + context (#1839) 2023-11-08 08:52:01 -08:00
tarikpnr bc64c86b12 Fix jump in suggested follows tab (Native) 2023-11-08 17:07:32 +03:00
Paul Frazee 2acc88e78d Move reminders to new persisted state layer (#1834) 2023-11-07 16:39:13 -08:00
Eric Bailey 96d8faf4b0 Add persistent state provider (#1830)
* Add persistent state provider

* Catch write error

* Handle read errors, update error msgs

* Fix lint

* Don't provide initial state to loader

* Remove colorMode from shell state

* Idea: hook into persisted context from other files

* Migrate settings to new hook

* Rework persisted state to split individual contexts

* Tweak persisted schema and validation

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-11-07 14:06:17 -08:00
Eric Bailey bfe196bac5 Extract shell state into separate context (#1824)
* WIP

* Add shell state

* Integrate new shell state for drawer and minimal shell mode

* Replace isDrawerSwipeDisabled

* Split shell state into separate contexts to avoid needless re-renders

* Fix typo

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-11-07 11:37:47 -08:00
Noritada Kobayashi 7158157f5f Correct meta tag attributes (#1829) 2023-11-07 09:56:44 -08:00
dan 7b2a7db83c Pull animated scroll handler down from pager (#1827) 2023-11-07 08:46:39 -08:00
dan fa821943da Use Sentry only for errors (#1776)
* Use Sentry only for errors

* Fix merge
2023-11-07 04:30:00 +00:00
Eric Bailey 7ffbee18b5 Fix removal of old lists from saved feeds (#1823)
* Fix removal of old lists from saved feeds

* Fix saved feed removal race condition
2023-11-06 18:51:46 -08:00
dan a4baf14e4b Fix accidental unwrapped strings (#1826) 2023-11-06 14:42:25 -08:00
dan d715246e26 Fix sticky pager jumps (#1825)
* Defer showing pager content until its header settles

* Introduce the concept of headerOnlyHeight

* Keep headerOnlyHeight in state, make headerHeight derived

* Hide content until *both* header (only) and tabbar are measured

* Hide tabbar to read its layout earlier

* Give consistent keys to pages
2023-11-06 22:30:10 +00:00
dan 4c00fc576d Fix minor list layout bugs (#1810)
* Fix accidental border on mobile

* Fix lists list to take full screen
2023-11-06 17:15:01 +00:00
Eric Bailey 95b5e642dc Merge pull request #1814 from bluesky-social/eric/replace-logger
Remove old logger
2023-11-04 14:00:38 -05:00
Eric Bailey f6fe980a64 Remove old logger 2023-11-04 13:43:28 -05:00
Eric Bailey f51351e80d Replace all logs with new logger 2023-11-04 13:42:36 -05:00
Eric Bailey e49a3d8a56 Merge pull request #1813 from bluesky-social/eric/app-903-extract-logger-into-singleton
Add new logger
2023-11-04 13:12:46 -05:00
Eric Bailey 0c76866757 Add logDump tests, ensure Log screen is reversed 2023-11-04 13:09:12 -05:00
Eric Bailey 7e29ebbadb Fix other error logs while I'm at it 2023-11-04 12:58:50 -05:00
Eric Bailey df0dcf32f9 Fix immediate TS errors 2023-11-04 12:42:27 -05:00
Eric Bailey 5fd41ad5a2 Comment out sentry in prod for now 2023-11-04 12:28:30 -05:00
Eric Bailey fd93bf2146 Add new logger 2023-11-04 12:18:56 -05:00
Paul Frazee 46c2564e65 Increment iOS buildnumber & android version code for release 2023-11-03 16:44:59 -07:00
Paul Frazee ebad6d2b1a ListAddUser modal UX improvements (#1809)
* typo

* Add loading state to ListAddUser

* Improve UI/UX of ListAddUser
2023-11-03 16:44:00 -07:00
Paul Frazee 4de852ded8 Refresh on scroll-to-top by active section press (#1807) 2023-11-03 14:53:47 -07:00
Paul Frazee 445f976881 Improved list and feed errors (#1798)
* Fix error-state rendering of ProfileList

* Unsave/unpin lists on delete

* Improve handling of failing feedgens

* Only show 'remove' btn on feed DNE
2023-11-03 14:18:44 -07:00
Eric Bailey 691af26895 Add subpath imports (#1808) 2023-11-03 14:17:40 -07:00
Paul Frazee 047f74c67d Fix copyright report link (#1799) 2023-11-02 22:09:31 -07:00
Paul Frazee 9437d743e2 Bump ios build number 2023-11-02 19:55:15 -07:00
Paul Frazee 846c130706 Lookup did for my profile at screen to avoid bad actor error (#1800) 2023-11-02 19:54:43 -07:00
dan 8747c215b1 Work around a startup stall caused by expo-image on low-end Android (#1801)
* Use plain RN image for startup-blocking UI

* Add comments
2023-11-02 19:33:18 -07:00
Paul Frazee 5e2025e60a Bump @atproto/api@0.6.23 2023-11-02 18:27:20 -07:00
Eric Bailey f258289be9 Treat PHP pages as HTML (#1797) 2023-11-02 15:16:31 -07:00
Paul Frazee 0f4bfcb05d Replace draglist due to upstream errors (#1795)
* Replace draggable flatlist with simple sort buttons

* Remove react-native-draggable-flatlist dep

* Fix hitslops

* Update lockfile

* Remove bad flex:1
2023-11-02 12:09:57 -07:00
dan 0e8723c3bb Patch facebook/metro#1130 to profile module init (#1796) 2023-11-02 18:48:07 +00:00
Paul Frazee 237bce391b Merge branch 'main' of github.com:bluesky-social/social-app into main 2023-11-02 09:43:47 -07:00
Paul Frazee c8b9407e0e Merge branch 'flicknow-make-choose-account-scrollable' into main 2023-11-02 09:43:37 -07:00
Paul Frazee b782cd0d06 Fix max-height issues 2023-11-02 09:42:29 -07:00
Paul Frazee 4e5a99d4d3 Merge branch 'make-choose-account-scrollable' of https://github.com/flicknow/social-app into flicknow-make-choose-account-scrollable 2023-11-02 09:34:49 -07:00
Spandan Tripathy d7744c2590 Update build.md (#1785) 2023-11-02 09:27:31 -07:00
Paul Frazee 29bfed78f7 Merge branch 'main' of github.com:bluesky-social/social-app into main 2023-11-01 21:14:47 -07:00
Paul Frazee 232e90c0a0 1.56 2023-11-01 21:14:39 -07:00
Paul Frazee 59271663b5 Store saved feeds on the root store so we can load on init (#1793) 2023-11-01 18:45:59 -07:00
flicknow 5f135aa021 Make choose account form scrollable 2023-11-01 20:55:06 -04:00
Paul Frazee f57a8cf8ba Lists updates: curate lists and blocklists (#1689)
* Add lists screen

* Update Lists screen and List create/edit modal to support curate lists

* Rework the ProfileList screen and add curatelist support

* More ProfileList progress

* Update list modals

* Rename mutelists to modlists

* Layout updates/fixes

* More layout fixes

* Modal fixes

* List list screen updates

* Update feed page to give more info

* Layout fixes to ListAddUser modal

* Layout fixes to FlatList and Feed on desktop

* Layout fix to LoadLatestBtn on Web

* Handle did resolution before showing the ProfileList screen

* Rename the CustomFeed routes to ProfileFeed for consistency

* Fix layout issues with the pager and feeds

* Factor out some common code

* Fix UIs for mobile

* Fix user list rendering

* Fix: dont bubble custom feed errors in the merge feed

* Refactor feed models to reduce usage of the SavedFeeds model

* Replace CustomFeedModel with FeedSourceModel which abstracts feed-generators and lists

* Add the ability to pin lists

* Add pinned lists to mobile

* Remove dead code

* Rework the ProfileScreenHeader to create more real-estate for action buttons

* Improve layout behavior on web mobile breakpoints

* Refactor feed & list pages to use new Tabs layout component

* Refactor to ProfileSubpageHeader

* Implement modlist block and mute

* Switch to new api and just modify state on modlist actions

* Fix some UI overflows

* Fix: dont show edit buttons on lists you dont own

* Fix alignment issue on long titles

* Improve loading and error states for feeds & lists

* Update list dropdown icons for ios

* Fetch feed display names in the mergefeed

* Improve rendering off offline feeds in the feed-listing page

* Update Feeds listing UI to react to changes in saved/pinned state

* Refresh list and feed on posts tab press

* Fix pinned feed ordering UI

* Fixes to list pinning

* Remove view=simple qp

* Add list to feed tuners

* Render richtext

* Add list href

* Add 'view avatar'

* Remove unused import

* Fix missing import

* Correctly reflect block by list state

* Replace the <Tabs> component with the more effective <PagerWithHeader> component

* Improve the responsiveness of the PagerWithHeader

* Fix visual jank in the feed loading state

* Improve performance of the PagerWithHeader

* Fix a case that would cause the header to animate too aggressively

* Add the ability to scroll to top by tapping the selected tab

* Fix unit test runner

* Update modlists test

* Add curatelist tests

* Fix: remove link behavior in ListAddUser modal

* Fix some layout jank in the PagerWithHeader on iOS

* Simplify ListItems header rendering

* Wait for the appview to recognize the list before proceeding with list creation

* Fix glitch in the onPageSelecting index of the Pager

* Fix until()

* Copy fix

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

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2023-11-01 16:15:40 -07:00
494 changed files with 40038 additions and 21385 deletions
+5
View File
@@ -1 +1,6 @@
# Copy this to `.env` and `.env.test` files
SENTRY_AUTH_TOKEN=
EXPO_PUBLIC_ENV=development
EXPO_PUBLIC_LOG_LEVEL=debug
EXPO_PUBLIC_LOG_DEBUG=
+3 -1
View File
@@ -7,7 +7,7 @@ module.exports = {
'prettier',
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'detox', 'react'],
plugins: ['@typescript-eslint', 'detox', 'react', 'lingui'],
rules: {
'react/no-unescaped-entities': 0,
'react-native/no-inline-styles': 0,
@@ -25,6 +25,8 @@ module.exports = {
'bskyweb',
'*.html',
'bskyweb',
'src/locale/locales/_build/',
'src/locale/locales/**/*.js',
],
settings: {
componentWrapperFunctions: ['observer'],
@@ -3,7 +3,8 @@ on:
push:
branches:
- main
- jake/bskyweb-additions
- traffic-reduction
- respect-optout-for-embeds
env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
USERNAME: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_USERNAME }}
+16 -4
View File
@@ -17,9 +17,15 @@ jobs:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Yarn install
run: yarn --frozen-lockfile
uses: Wandalen/wretry.action@master
with:
command: yarn --frozen-lockfile
attempt_limit: 3
attempt_delay: 2000
- name: Lint check
run: yarn lint
- name: Check & compile i18n
run: yarn intl:build
- name: Type check
run: yarn typecheck
testing:
@@ -27,13 +33,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install node 18
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
- name: Check out Git repository
uses: actions/checkout@v3
- name: Yarn install
run: yarn --frozen-lockfile
uses: Wandalen/wretry.action@master
with:
command: yarn --frozen-lockfile
attempt_limit: 3
attempt_delay: 2000
- name: Check & compile i18n
run: yarn intl:build
- name: Run tests
run: |
yarn test --forceExit
NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test --forceExit
+5 -1
View File
@@ -102,4 +102,8 @@ ios/
google-services.json
# Performance results (Flashlight)
.perf/
.perf/
# i18n
src/locale/locales/_build/
src/locale/locales/**/*.js
+1
View File
@@ -31,6 +31,7 @@ RUN \. "$NVM_DIR/nvm.sh" && \
nvm use $NODE_VERSION && \
npm install --global yarn && \
yarn && \
yarn intl:compile && \
yarn build-web
# DEBUG
+2 -1
View File
@@ -10,11 +10,12 @@ help: ## Print info about all commands
.PHONY: build-web
build-web: ## Compile web bundle, copy to bskyweb directory
yarn intl:build
yarn build-web
.PHONY: test
test: ## Run all tests
yarn test
NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test
.PHONY: lint
lint: ## Run style checks and verify syntax
+4
View File
@@ -1,5 +1,7 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer, sleep} from '../util'
describe('Composer', () => {
@@ -45,6 +47,8 @@ describe('Composer', () => {
})
it('Reply text only', async () => {
await element(by.id('e2eRefreshHome')).tap()
const post = by.id('feedItem-by-alice.test')
await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap()
await element(by.id('composerTextInput')).typeText('Reply text only')
+10
View File
@@ -1,5 +1,7 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, createServer} from '../util'
describe('Create account', () => {
@@ -10,6 +12,8 @@ describe('Create account', () => {
})
it('I can create a new account', async () => {
await element(by.id('e2eOpenLoggedOutView')).tap()
await element(by.id('createAccountButton')).tap()
await device.takeScreenshot('1- opened create account screen')
await element(by.id('otherServerBtn')).tap()
@@ -17,14 +21,20 @@ describe('Create account', () => {
await element(by.id('customServerInput')).clearText()
await element(by.id('customServerInput')).typeText(service)
await device.takeScreenshot('3- input test server URL')
await element(by.id('nextBtn')).tap()
await element(by.id('emailInput')).typeText('example@test.com')
await element(by.id('passwordInput')).typeText('hunter2')
await device.takeScreenshot('4- entered account details')
await element(by.id('nextBtn')).tap()
await element(by.id('handleInput')).typeText('e2e-test')
await device.takeScreenshot('4- entered handle')
await element(by.id('nextBtn')).tap()
await expect(element(by.id('welcomeOnboarding'))).toBeVisible()
await element(by.id('continueBtn')).tap()
await expect(element(by.id('recommendedFeedsOnboarding'))).toBeVisible()
+211
View File
@@ -0,0 +1,211 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, loginAsBob, createServer, sleep} from '../util'
describe('Curate lists', () => {
beforeAll(async () => {
await createServer('?users&follows&posts')
await openApp({
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
})
})
it('Login and create a curatelists', async () => {
await loginAsAlice()
await element(by.id('e2eGotoLists')).tap()
await element(by.id('newUserListBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('editNameInput')).typeText('Good Ppl')
await element(by.id('editDescriptionInput')).typeText('They good')
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
await element(by.text('About')).tap()
await expect(element(by.id('headerTitle'))).toHaveText('Good Ppl')
await expect(element(by.id('listDescription'))).toHaveText('They good')
})
it('Edit display name and description via the edit curatelist modal', async () => {
await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('editNameInput')).clearText()
await element(by.id('editNameInput')).typeText('Bad Ppl')
await element(by.id('editDescriptionInput')).clearText()
await element(by.id('editDescriptionInput')).typeText('They bad')
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl')
await expect(element(by.id('listDescription'))).toHaveText('They bad')
// have to wait for the toast to clear
await waitFor(element(by.id('headerDropdownBtn')))
.toBeVisible()
.withTimeout(5000)
})
it('Remove description via the edit curatelist modal', async () => {
await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('editDescriptionInput')).clearText()
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
await expect(element(by.id('listDescription'))).not.toBeVisible()
// have to wait for the toast to clear
await waitFor(element(by.id('headerDropdownBtn')))
.toBeVisible()
.withTimeout(5000)
})
it('Set avi via the edit curatelist modal', async () => {
await expect(element(by.id('userAvatarFallback'))).toExist()
await element(by.id('headerDropdownBtn')).tap()
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 sleep(3e3)
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
await expect(element(by.id('userAvatarImage'))).toExist()
// have to wait for the toast to clear
await waitFor(element(by.id('headerDropdownBtn')))
.toBeVisible()
.withTimeout(5000)
})
it('Remove avi via the edit curatelist modal', async () => {
await expect(element(by.id('userAvatarImage'))).toExist()
await element(by.id('headerDropdownBtn')).tap()
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.id('saveBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
await expect(element(by.id('userAvatarFallback'))).toExist()
// have to wait for the toast to clear
await waitFor(element(by.id('headerDropdownBtn')))
.toBeVisible()
.withTimeout(5000)
})
it('Delete the curatelist', async () => {
await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Delete List')).tap()
await element(by.id('confirmBtn')).tap()
await expect(element(by.id('listsEmpty'))).toBeVisible()
})
it('Create a new curatelist', async () => {
await element(by.id('e2eGotoLists')).tap()
await element(by.id('newUserListBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('editNameInput')).typeText('Good Ppl')
await element(by.id('editDescriptionInput')).typeText('They good')
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
await element(by.text('About')).tap()
await expect(element(by.id('headerTitle'))).toHaveText('Good Ppl')
await expect(element(by.id('listDescription'))).toHaveText('They good')
})
it('Adds users on curatelists from the list', async () => {
await element(by.text('About')).tap()
await element(by.id('addUserBtn')).tap()
await expect(element(by.id('listAddUserModal'))).toBeVisible()
await waitFor(element(by.id('user-bob.test-addBtn')))
.toBeVisible()
.withTimeout(5000)
await element(by.id('user-bob.test-addBtn')).tap()
await element(by.id('doneBtn')).tap()
await expect(element(by.id('listAddUserModal'))).not.toBeVisible()
await expect(element(by.id('user-bob.test'))).toBeVisible()
})
it('Shows posts by the users in the list', async () => {
await element(by.text('Posts')).tap()
await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible()
})
it('Pins the list', async () => {
await expect(element(by.id('pinBtn'))).toBeVisible()
await element(by.id('pinBtn')).tap()
await element(by.id('e2eGotoHome')).tap()
await element(by.id('homeScreenFeedTabs-Good Ppl')).tap()
await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible()
await element(by.id('bottomBarFeedsBtn')).tap()
await element(by.id('saved-feed-Good Ppl')).tap()
await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible()
await element(by.id('unpinBtn')).tap()
await element(by.id('bottomBarHomeBtn')).tap()
await expect(
element(by.id('homeScreenFeedTabs-Good Ppl')),
).not.toBeVisible()
await element(by.id('e2eGotoLists')).tap()
await element(by.id('list-Good Ppl')).tap()
})
it('Removes users on curatelists from the list', async () => {
await element(by.text('About')).tap()
await expect(element(by.id('user-bob.test'))).toBeVisible()
await element(by.id('user-bob.test-editBtn')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
await element(by.id('user-bob.test-addBtn')).tap()
await element(by.id('doneBtn')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
})
it('Shows the curatelist on my profile', async () => {
await element(by.id('bottomBarProfileBtn')).tap()
await element(by.id('profilePager-selector')).swipe('left')
await element(by.id('profilePager-selector-5')).tap()
await element(by.id('list-Good Ppl')).tap()
})
it('Adds and removes users on curatelists from the profile', async () => {
await element(by.id('bottomBarSearchBtn')).tap()
await element(by.id('searchTextInput')).typeText('bob')
await element(by.id('searchAutoCompleteResult-bob.test')).tap()
await expect(element(by.id('profileView'))).toBeVisible()
await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.text('Add to Lists')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
await element(by.id('user-bob.test-addBtn')).tap()
await element(by.id('doneBtn')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.text('Add to Lists')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
await element(by.id('user-bob.test-addBtn')).tap()
await element(by.id('doneBtn')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
})
it('Can report a user list', async () => {
await element(by.id('e2eGotoSettings')).tap()
await element(by.id('signOutBtn')).tap()
await loginAsBob()
await element(by.id('bottomBarSearchBtn')).tap()
await element(by.id('searchTextInput')).typeText('alice')
await element(by.id('searchAutoCompleteResult-alice.test')).tap()
await element(by.id('profilePager-selector')).swipe('left')
await element(by.id('profilePager-selector-3')).tap()
await element(by.id('list-Good Ppl')).tap()
await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Report List')).tap()
await expect(element(by.id('reportModal'))).toBeVisible()
await expect(element(by.text('Report List'))).toBeVisible()
await element(
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonRude'),
).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportModal'))).not.toBeVisible()
})
})
+23 -4
View File
@@ -1,10 +1,12 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer} from '../util'
describe('Home screen', () => {
beforeAll(async () => {
await createServer('?users&follows&posts')
await createServer('?users&follows&posts&feeds')
await openApp({permissions: {notifications: 'YES'}})
})
@@ -13,6 +15,23 @@ describe('Home screen', () => {
await element(by.id('homeScreenFeedTabs-Following')).tap()
})
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()
})
it('Feeds button disappears after pinning a feed', async () => {
await element(by.id('bottomBarProfileBtn')).tap()
await element(by.id('profilePager-selector')).swipe('left')
await element(by.id('profilePager-selector-4')).tap()
await element(by.id('feed-alice-favs')).tap()
await element(by.id('pinBtn')).tap()
await element(by.id('bottomBarHomeBtn')).tap()
await expect(
element(by.id('homeScreenFeedTabs-Feeds ✨')),
).not.toBeVisible()
})
it('Can like posts', async () => {
const carlaPosts = by.id('feedItem-by-carla.test')
await expect(
@@ -65,14 +84,14 @@ describe('Home screen', () => {
it('Can swipe between feeds', async () => {
await element(by.id('homeScreen')).swipe('left', 'fast', 0.75)
await expect(element(by.id('whatshotFeedPage'))).toBeVisible()
await expect(element(by.id('customFeedPage'))).toBeVisible()
await element(by.id('homeScreen')).swipe('right', 'fast', 0.75)
await expect(element(by.id('followingFeedPage'))).toBeVisible()
})
it('Can tap between feeds', async () => {
await element(by.id("homeScreenFeedTabs-What's hot")).tap()
await expect(element(by.id('whatshotFeedPage'))).toBeVisible()
await element(by.id('homeScreenFeedTabs-alice-favs')).tap()
await expect(element(by.id('customFeedPage'))).toBeVisible()
await element(by.id('homeScreenFeedTabs-Following')).tap()
await expect(element(by.id('followingFeedPage'))).toBeVisible()
})
+3 -16
View File
@@ -5,6 +5,8 @@
* with the side drawer.
*/
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer} from '../util'
describe('invite-codes', () => {
@@ -16,7 +18,6 @@ describe('invite-codes', () => {
})
it('I can fetch invite codes', async () => {
await expect(element(by.id('signInButton'))).toBeVisible()
await loginAsAlice()
await element(by.id('e2eOpenInviteCodesModal')).tap()
await expect(element(by.id('inviteCodesModal'))).toBeVisible()
@@ -27,6 +28,7 @@ describe('invite-codes', () => {
})
it('I can create a new account with the invite code', async () => {
await element(by.id('e2eOpenLoggedOutView')).tap()
await element(by.id('createAccountButton')).tap()
await device.takeScreenshot('1- opened create account screen')
await element(by.id('otherServerBtn')).tap()
@@ -51,19 +53,4 @@ describe('invite-codes', () => {
await element(by.id('continueBtn')).tap()
await expect(element(by.id('homeScreen'))).toBeVisible()
})
it('I get a notification for the new user', async () => {
await element(by.id('e2eSignOut')).tap()
await loginAsAlice()
await waitFor(element(by.id('homeScreen')))
.toBeVisible()
.withTimeout(5000)
await element(by.id('bottomBarNotificationsBtn')).tap()
await expect(element(by.id('invitedUser'))).toBeVisible()
})
it('I can dismiss the new user notification', async () => {
await element(by.id('dismissBtn')).tap()
await expect(element(by.id('invitedUser'))).not.toBeVisible()
})
})
+4
View File
@@ -1,5 +1,7 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, login, createServer} from '../util'
describe('Login', () => {
@@ -10,6 +12,8 @@ describe('Login', () => {
})
it('As Alice, I can login', async () => {
await element(by.id('e2eOpenLoggedOutView')).tap()
await expect(element(by.id('signInButton'))).toBeVisible()
await login(service, 'alice', 'hunter2', {
takeScreenshots: true,
@@ -1,5 +1,7 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer} from '../util'
describe('Mergefeed', () => {
@@ -9,8 +11,12 @@ describe('Mergefeed', () => {
})
it('Login', async () => {
await element(by.id('e2eOpenLoggedOutView')).tap()
await loginAsAlice()
await element(by.id('e2eToggleMergefeed')).tap()
await element(by.id('bottomBarFeedsBtn')).tap()
await element(by.id('feed-alice-favs-toggleSave')).tap()
await element(by.id('e2eGotoHome')).tap()
})
it('Sees the expected mix of posts with default filters', async () => {
+188
View File
@@ -0,0 +1,188 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, loginAsBob, createServer, sleep} from '../util'
describe('Mod lists', () => {
beforeAll(async () => {
await createServer('?users&follows&labels')
await openApp({
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
})
})
it('Login and view my modlists', async () => {
await loginAsAlice()
await element(by.id('e2eGotoModeration')).tap()
await element(by.id('moderationlistsBtn')).tap()
await expect(element(by.id('list-Muted Users'))).toBeVisible()
await element(by.id('list-Muted Users')).tap()
await expect(
element(by.id('user-muted-by-list-account.test')),
).toBeVisible()
})
it('Toggle mute subscription', async () => {
await element(by.id('unmuteBtn')).tap()
await element(by.id('subscribeBtn')).tap()
await element(by.text('Mute accounts')).tap()
await element(by.id('confirmBtn')).tap()
})
it('Edit display name and description via the edit modlist modal', async () => {
await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('editNameInput')).clearText()
await element(by.id('editNameInput')).typeText('Bad Ppl')
await element(by.id('editDescriptionInput')).clearText()
await element(by.id('editDescriptionInput')).typeText('They bad')
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl')
await expect(element(by.id('listDescription'))).toHaveText('They bad')
// have to wait for the toast to clear
await waitFor(element(by.id('headerDropdownBtn')))
.toBeVisible()
.withTimeout(5000)
})
it('Remove description via the edit modlist modal', async () => {
await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('editDescriptionInput')).clearText()
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
await expect(element(by.id('listDescription'))).not.toBeVisible()
// have to wait for the toast to clear
await waitFor(element(by.id('headerDropdownBtn')))
.toBeVisible()
.withTimeout(5000)
})
it('Set avi via the edit modlist modal', async () => {
await expect(element(by.id('userAvatarFallback'))).toExist()
await element(by.id('headerDropdownBtn')).tap()
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 sleep(3e3)
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
await expect(element(by.id('userAvatarImage'))).toExist()
// have to wait for the toast to clear
await waitFor(element(by.id('headerDropdownBtn')))
.toBeVisible()
.withTimeout(5000)
})
it('Remove avi via the edit modlist modal', async () => {
await expect(element(by.id('userAvatarImage'))).toExist()
await element(by.id('headerDropdownBtn')).tap()
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.id('saveBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
await expect(element(by.id('userAvatarFallback'))).toExist()
// have to wait for the toast to clear
await waitFor(element(by.id('headerDropdownBtn')))
.toBeVisible()
.withTimeout(5000)
})
it('Delete the modlist', async () => {
await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Delete List')).tap()
await element(by.id('confirmBtn')).tap()
await expect(element(by.id('listsEmpty'))).toBeVisible()
})
it('Create a new modlist', async () => {
await element(by.id('newModListBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('editNameInput')).typeText('Bad Ppl')
await element(by.id('editDescriptionInput')).typeText('They bad')
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).not.toBeVisible()
await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl')
await expect(element(by.id('listDescription'))).toHaveText('They bad')
})
it('Adds and removes users on modlists from the list', async () => {
await element(by.id('addUserBtn')).tap()
await expect(element(by.id('listAddUserModal'))).toBeVisible()
await waitFor(element(by.id('user-warn-posts.test-addBtn')))
.toBeVisible()
.withTimeout(5000)
await element(by.id('user-warn-posts.test-addBtn')).tap()
await element(by.id('doneBtn')).tap()
await expect(element(by.id('listAddUserModal'))).not.toBeVisible()
await element(by.id('listItems-flatlist')).swipe(
'down',
'slow',
1,
0.5,
0.5,
)
await expect(element(by.id('user-warn-posts.test'))).toBeVisible()
await element(by.id('user-warn-posts.test-editBtn')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
await element(by.id('user-warn-posts.test-addBtn')).tap()
await element(by.id('doneBtn')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
})
it('Shows the modlist on my profile', async () => {
await element(by.id('bottomBarProfileBtn')).tap()
await element(by.id('profilePager-selector')).swipe('left')
await element(by.id('profilePager-selector-5')).tap()
await element(by.id('list-Bad Ppl')).tap()
})
it('Adds and removes users on modlists from the profile', async () => {
await element(by.id('bottomBarSearchBtn')).tap()
await element(by.id('searchTextInput')).typeText('bob')
await element(by.id('searchAutoCompleteResult-bob.test')).tap()
await expect(element(by.id('profileView'))).toBeVisible()
await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.text('Add to Lists')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
await element(by.id('user-bob.test-addBtn')).tap()
await element(by.id('doneBtn')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.text('Add to Lists')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
await element(by.id('user-bob.test-addBtn')).tap()
await element(by.id('doneBtn')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
})
it('Can report a mute list', async () => {
await element(by.id('e2eGotoSettings')).tap()
await element(by.id('signOutBtn')).tap()
await loginAsBob()
await element(by.id('bottomBarSearchBtn')).tap()
await element(by.id('searchTextInput')).typeText('alice')
await element(by.id('searchAutoCompleteResult-alice.test')).tap()
await element(by.id('profilePager-selector')).swipe('left')
await element(by.id('profilePager-selector-3')).tap()
await element(by.id('list-Bad Ppl')).tap()
await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Report List')).tap()
await expect(element(by.id('reportModal'))).toBeVisible()
await expect(element(by.text('Report List'))).toBeVisible()
await element(
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonRude'),
).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportModal'))).not.toBeVisible()
})
})
-159
View File
@@ -1,159 +0,0 @@
/* eslint-env detox/detox */
import {openApp, loginAsAlice, loginAsBob, createServer, sleep} from '../util'
describe('Mute lists', () => {
beforeAll(async () => {
await createServer('?users&follows&labels')
await openApp({
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
})
})
it('Login and view my mutelists', async () => {
await expect(element(by.id('signInButton'))).toBeVisible()
await loginAsAlice()
await element(by.id('e2eGotoModeration')).tap()
await element(by.id('mutelistsBtn')).tap()
await expect(element(by.id('list-Muted Users'))).toBeVisible()
await element(by.id('list-Muted Users')).tap()
await expect(
element(by.id('user-muted-by-list-account.test')),
).toBeVisible()
})
it('Toggle subscription', async () => {
await element(by.id('unsubscribeListBtn')).tap()
await element(by.id('subscribeListBtn')).tap()
})
it('Edit display name and description via the edit mutelist modal', async () => {
await element(by.id('editListBtn')).tap()
await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible()
await element(by.id('editNameInput')).clearText()
await element(by.id('editNameInput')).typeText('Bad Ppl')
await element(by.id('editDescriptionInput')).clearText()
await element(by.id('editDescriptionInput')).typeText('They bad')
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible()
await expect(element(by.id('listName'))).toHaveText('Bad Ppl')
await expect(element(by.id('listDescription'))).toHaveText('They bad')
// have to wait for the toast to clear
await waitFor(element(by.id('editListBtn')))
.toBeVisible()
.withTimeout(5000)
})
it('Remove description via the edit mutelist modal', async () => {
await element(by.id('editListBtn')).tap()
await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible()
await element(by.id('editDescriptionInput')).clearText()
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible()
await expect(element(by.id('listDescription'))).not.toBeVisible()
// have to wait for the toast to clear
await waitFor(element(by.id('editListBtn')))
.toBeVisible()
.withTimeout(5000)
})
it('Set avi via the edit mutelist modal', async () => {
await expect(element(by.id('userAvatarFallback'))).toExist()
await element(by.id('editListBtn')).tap()
await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible()
await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Library')).tap()
await sleep(3e3)
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible()
await expect(element(by.id('userAvatarImage'))).toExist()
// have to wait for the toast to clear
await waitFor(element(by.id('editListBtn')))
.toBeVisible()
.withTimeout(5000)
})
it('Remove avi via the edit mutelist modal', async () => {
await expect(element(by.id('userAvatarImage'))).toExist()
await element(by.id('editListBtn')).tap()
await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible()
await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Remove')).tap()
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible()
await expect(element(by.id('userAvatarFallback'))).toExist()
// have to wait for the toast to clear
await waitFor(element(by.id('editListBtn')))
.toBeVisible()
.withTimeout(5000)
})
it('Delete the mutelist', async () => {
await element(by.id('deleteListBtn')).tap()
await element(by.id('confirmBtn')).tap()
await expect(element(by.id('emptyMuteLists'))).toBeVisible()
})
it('Create a new mutelist', async () => {
await element(by.id('emptyMuteLists-button')).tap()
await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible()
await element(by.id('editNameInput')).typeText('Bad Ppl')
await element(by.id('editDescriptionInput')).typeText('They bad')
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible()
await expect(element(by.id('listName'))).toHaveText('Bad Ppl')
await expect(element(by.id('listDescription'))).toHaveText('They bad')
// have to wait for the toast to clear
await waitFor(element(by.id('editListBtn')))
.toBeVisible()
.withTimeout(5000)
})
it('Shows the mutelist on my profile', async () => {
await element(by.id('bottomBarProfileBtn')).tap()
await element(by.id('selector')).swipe('left')
await element(by.id('selector-4')).tap()
await element(by.id('list-Bad Ppl')).tap()
})
it('Adds and removes users on mutelists', async () => {
await element(by.id('bottomBarSearchBtn')).tap()
await element(by.id('searchTextInput')).typeText('bob')
await element(by.id('searchAutoCompleteResult-bob.test')).tap()
await expect(element(by.id('profileView'))).toBeVisible()
await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.text('Add to Lists')).tap()
await expect(element(by.id('listAddRemoveUserModal'))).toBeVisible()
await element(by.id('toggleBtn-Bad Ppl')).tap()
await element(by.id('saveBtn')).tap()
await expect(element(by.id('listAddRemoveUserModal'))).not.toBeVisible()
await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.text('Add to Lists')).tap()
await expect(element(by.id('listAddRemoveUserModal'))).toBeVisible()
await element(by.id('toggleBtn-Bad Ppl')).tap()
await element(by.id('saveBtn')).tap()
await expect(element(by.id('listAddRemoveUserModal'))).not.toBeVisible()
})
it('Can report a mute list', async () => {
await element(by.id('e2eGotoSettings')).tap()
await element(by.id('signOutBtn')).tap()
await loginAsBob()
await element(by.id('bottomBarSearchBtn')).tap()
await element(by.id('searchTextInput')).typeText('alice')
await element(by.id('searchAutoCompleteResult-alice.test')).tap()
await element(by.id('selector')).swipe('left')
await element(by.id('selector-3')).tap()
await element(by.id('list-Bad Ppl')).tap()
await element(by.id('reportListBtn')).tap()
await expect(element(by.id('reportModal'))).toBeVisible()
await expect(element(by.text('Report List'))).toBeVisible()
await element(
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonRude'),
).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportModal'))).not.toBeVisible()
})
})
+14 -5
View File
@@ -1,5 +1,7 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer, sleep} from '../util'
describe('Profile screen', () => {
@@ -11,17 +13,16 @@ describe('Profile screen', () => {
})
it('Login and navigate to my profile', async () => {
await expect(element(by.id('signInButton'))).toBeVisible()
await loginAsAlice()
await element(by.id('bottomBarProfileBtn')).tap()
})
it('Can see feeds', async () => {
await element(by.id('selector')).swipe('left')
await element(by.id('selector-4')).tap()
await element(by.id('profilePager-selector')).swipe('left')
await element(by.id('profilePager-selector-4')).tap()
await expect(element(by.id('feed-alice-favs'))).toBeVisible()
await element(by.id('selector')).swipe('right')
await element(by.id('selector-0')).tap()
await element(by.id('profilePager-selector')).swipe('right')
await element(by.id('profilePager-selector-0')).tap()
})
it('Open and close edit profile modal', async () => {
@@ -135,6 +136,14 @@ describe('Profile screen', () => {
})
it('Can like posts', async () => {
await element(by.id('postsFeed-flatlist')).swipe(
'down',
'slow',
1,
0.5,
0.5,
)
const posts = by.id('feedItem-by-bob.test')
await expect(
element(by.id('likeCount').withAncestor(posts)).atIndex(0),
+2
View File
@@ -1,5 +1,7 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer} from '../util'
describe('Search screen', () => {
+2
View File
@@ -1,5 +1,7 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer, sleep} from '../util'
describe('Self-labeling', () => {
+3 -1
View File
@@ -1,5 +1,7 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, loginAsBob, createServer} from '../util'
describe('Thread muting', () => {
@@ -48,7 +50,7 @@ describe('Thread muting', () => {
await loginAsBob()
await element(by.id('bottomBarProfileBtn')).tap()
await element(by.id('selector-1')).tap()
await element(by.id('profilePager-selector-1')).tap()
const bobPosts = by.id('feedItem-by-bob.test')
await element(by.id('replyBtn').withAncestor(bobPosts)).atIndex(0).tap()
await element(by.id('composerTextInput')).typeText('Reply 2')
+8 -6
View File
@@ -1,5 +1,7 @@
/* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer} from '../util'
describe('Thread screen', () => {
@@ -31,15 +33,15 @@ describe('Thread screen', () => {
it('Can like the root post', async () => {
const post = by.id('postThreadItem-by-bob.test')
await expect(
element(by.id('likeCount').withAncestor(post)).atIndex(0),
element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0),
).not.toExist()
await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
await expect(
element(by.id('likeCount').withAncestor(post)).atIndex(0),
element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0),
).toHaveText('1 like')
await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
await expect(
element(by.id('likeCount').withAncestor(post)).atIndex(0),
element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0),
).not.toExist()
})
@@ -61,21 +63,21 @@ describe('Thread screen', () => {
it('Can repost the root post', async () => {
const post = by.id('postThreadItem-by-bob.test')
await expect(
element(by.id('repostCount').withAncestor(post)).atIndex(0),
element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0),
).not.toExist()
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
await expect(element(by.id('repostModal'))).toBeVisible()
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
await expect(element(by.id('repostModal'))).not.toBeVisible()
await expect(
element(by.id('repostCount').withAncestor(post)).atIndex(0),
element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0),
).toHaveText('1 repost')
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
await expect(element(by.id('repostModal'))).toBeVisible()
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
await expect(element(by.id('repostModal'))).not.toBeVisible()
await expect(
element(by.id('repostCount').withAncestor(post)).atIndex(0),
element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0),
).not.toExist()
})
-7
View File
@@ -1,10 +1,3 @@
jest.mock('sentry-expo', () => ({
init: () => jest.fn(),
Native: {
ReactNativeTracing: jest.fn().mockImplementation(() => ({
start: jest.fn(),
stop: jest.fn(),
})),
ReactNavigationInstrumentation: jest.fn(),
},
}))
@@ -1,3 +1,5 @@
import {it, describe, expect} from '@jest/globals'
import {
linkRequiresWarning,
isPossiblyAUrl,
@@ -6,6 +8,7 @@ import {
describe('linkRequiresWarning', () => {
type Case = [string, string, boolean]
const cases: Case[] = [
['http://example.com', 'http://example.com', false],
['http://example.com', 'example.com', false],
@@ -64,6 +67,10 @@ describe('linkRequiresWarning', () => {
['http://bsky.app/', 'https://google.com', true],
['https://bsky.app/', 'https://google.com', true],
// case insensitive
['https://Example.com', 'example.com', false],
['https://example.com', 'Example.com', false],
// bad uri inputs, default to true
['', '', true],
['example.com', 'example.com', true],
+46 -8
View File
@@ -1,12 +1,41 @@
const pkg = require('./package.json')
module.exports = function () {
const hasSentryToken = !!process.env.SENTRY_AUTH_TOKEN
/**
* App version number. Should be incremented as part of a release cycle.
*/
const VERSION = pkg.version
/**
* iOS build number. Must be incremented for each TestFlight version.
*/
const IOS_BUILD_NUMBER = '4'
/**
* Android build number. Must be incremented for each release.
*/
const ANDROID_VERSION_CODE = 50
/**
* Uses built-in Expo env vars
*
* @see https://docs.expo.dev/build-reference/variables/#built-in-environment-variables
*/
const PLATFORM = process.env.EAS_BUILD_PLATFORM
/**
* Additional granularity for the `dist` field
*/
const DIST_BUILD_NUMBER =
PLATFORM === 'android' ? ANDROID_VERSION_CODE : IOS_BUILD_NUMBER
return {
expo: {
version: VERSION,
name: 'Bluesky',
slug: 'bluesky',
scheme: 'bluesky',
owner: 'blueskysocial',
version: '1.55.0',
runtimeVersion: {
policy: 'appVersion',
},
@@ -14,12 +43,12 @@ module.exports = function () {
icon: './assets/icon.png',
userInterfaceStyle: 'automatic',
splash: {
image: './assets/cloud-splash.png',
image: './assets/splash.png',
resizeMode: 'cover',
backgroundColor: '#ffffff',
},
ios: {
buildNumber: '1',
buildNumber: IOS_BUILD_NUMBER,
supportsTablet: false,
bundleIdentifier: 'xyz.blueskyweb.app',
config: {
@@ -43,10 +72,13 @@ module.exports = function () {
backgroundColor: '#ffffff',
},
android: {
versionCode: 44,
versionCode: ANDROID_VERSION_CODE,
icon: './assets/icon.png',
adaptiveIcon: {
foregroundImage: './assets/adaptive-icon.png',
backgroundColor: '#ffffff',
foregroundImage: './assets/icon-android-foreground.png',
monochromeImage: './assets/icon-android-foreground.png',
backgroundImage: './assets/icon-android-background.png',
backgroundColor: '#1185FE',
},
googleServicesFile: './google-services.json',
package: 'xyz.blueskyweb.app',
@@ -74,7 +106,7 @@ module.exports = function () {
},
plugins: [
'expo-localization',
hasSentryToken && 'sentry-expo',
Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo',
[
'expo-build-properties',
{
@@ -92,6 +124,7 @@ module.exports = function () {
username: 'blueskysocial',
},
],
'./plugins/withAndroidManifestPlugin.js',
].filter(Boolean),
extra: {
eas: {
@@ -100,11 +133,16 @@ module.exports = function () {
},
hooks: {
postPublish: [
/*
* @see https://docs.expo.dev/guides/using-sentry/#app-configuration
*/
{
file: 'sentry-expo/upload-sourcemaps',
config: {
organization: 'blueskyweb',
project: 'react-native',
release: VERSION,
dist: `${PLATFORM}.${VERSION}.${DIST_BUILD_NUMBER}`,
},
},
],
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 452 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

+2
View File
@@ -37,6 +37,7 @@ module.exports = function (api) {
{
alias: {
// This needs to be mirrored in tsconfig.json
'#': './src',
lib: './src/lib',
platform: './src/platform',
state: './src/state',
@@ -44,6 +45,7 @@ module.exports = function (api) {
},
},
],
'macros',
'react-native-reanimated/plugin', // NOTE: this plugin MUST be last
],
env: {
+1 -1
View File
@@ -42,4 +42,4 @@ check: ## Compile everything, checking syntax (does not output binaries)
.PHONY: run-dev-bskyweb
run-dev-bskyweb: .env ## Runs 'bskyweb' for local dev
GOLOG_LOG_LEVEL=info go run ./cmd/bskyweb serve --debug
GOLOG_LOG_LEVEL=info go run ./cmd/bskyweb serve
+99
View File
@@ -0,0 +1,99 @@
package main
import (
"fmt"
"net/http"
appbsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/labstack/echo/v4"
)
// We don't actually populate the title for "posts".
// Some background: https://book.micro.blog/rss-for-microblogs/
type Item struct {
Title string `xml:"title,omitempty"`
Link string `xml:"link,omitempty"`
Description string `xml:"description,omitempty"`
PubDate string `xml:"pubDate,omitempty"`
Author string `xml:"author,omitempty"`
GUID string `xml:"guid,omitempty"`
}
type rss struct {
Version string `xml:"version,attr"`
Description string `xml:"channel>description,omitempty"`
Link string `xml:"channel>link"`
Title string `xml:"channel>title"`
Item []Item `xml:"channel>item"`
}
func (srv *Server) WebProfileRSS(c echo.Context) error {
ctx := c.Request().Context()
didParam := c.Param("did")
did, err := syntax.ParseDID(didParam)
if err != nil {
return echo.NewHTTPError(400, fmt.Sprintf("not a valid DID: %s", didParam))
}
// check that public view is Ok
pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, did.String())
if err != nil {
return echo.NewHTTPError(404, fmt.Sprintf("account not found: %s", did))
}
for _, label := range pv.Labels {
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
return echo.NewHTTPError(403, fmt.Sprintf("account does not allow public views: %s", did))
}
}
af, err := appbsky.FeedGetAuthorFeed(ctx, srv.xrpcc, did.String(), "", "", 30)
if err != nil {
log.Warn("failed to fetch author feed", "did", did, "err", err)
return err
}
posts := []Item{}
for _, p := range af.Feed {
// only include author's own posts in RSS
if p.Post.Author.Did != pv.Did {
continue
}
aturi, err := syntax.ParseATURI(p.Post.Uri)
if err != nil {
return err
}
rec := p.Post.Record.Val.(*appbsky.FeedPost)
// only top-level posts in RSS (no replies)
if rec.Reply != nil {
continue
}
posts = append(posts, Item{
Link: fmt.Sprintf("https://bsky.app/profile/%s/post/%s", pv.Handle, aturi.RecordKey().String()),
Description: rec.Text,
PubDate: rec.CreatedAt,
Author: "@" + pv.Handle,
GUID: aturi.String(),
})
}
title := "@" + pv.Handle
if pv.DisplayName != nil {
title = title + " - " + *pv.DisplayName
}
desc := ""
if pv.Description != nil {
desc = *pv.Description
}
feed := &rss{
Version: "2.0",
Description: desc,
Link: fmt.Sprintf("https://bsky.app/profile/%s", pv.Handle),
Title: title,
Item: posts,
}
return c.XML(http.StatusOK, feed)
}
+76 -40
View File
@@ -15,7 +15,8 @@ import (
"time"
appbsky "github.com/bluesky-social/indigo/api/bsky"
cliutil "github.com/bluesky-social/indigo/cmd/gosky/util"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/bluesky-social/indigo/util/cliutil"
"github.com/bluesky-social/indigo/xrpc"
"github.com/bluesky-social/social-app/bskyweb"
@@ -181,8 +182,9 @@ func serve(cctx *cli.Context) error {
e.GET("/search", server.WebGeneric)
e.GET("/feeds", server.WebGeneric)
e.GET("/notifications", server.WebGeneric)
e.GET("/lists", server.WebGeneric)
e.GET("/moderation", server.WebGeneric)
e.GET("/moderation/mute-lists", server.WebGeneric)
e.GET("/moderation/modlists", server.WebGeneric)
e.GET("/moderation/muted-accounts", server.WebGeneric)
e.GET("/moderation/blocked-accounts", server.WebGeneric)
e.GET("/settings", server.WebGeneric)
@@ -207,6 +209,9 @@ func serve(cctx *cli.Context) error {
e.GET("/profile/:handle/feed/:rkey", server.WebGeneric)
e.GET("/profile/:handle/feed/:rkey/liked-by", server.WebGeneric)
// profile RSS feed (DID not handle)
e.GET("/profile/:did/rss", server.WebProfileRSS)
// post endpoints; only first populates info
e.GET("/profile/:handle/post/:rkey", server.WebPost)
e.GET("/profile/:handle/post/:rkey/liked-by", server.WebGeneric)
@@ -284,56 +289,87 @@ func (srv *Server) WebHome(c echo.Context) error {
}
func (srv *Server) WebPost(c echo.Context) error {
ctx := c.Request().Context()
data := pongo2.Context{}
handle := c.Param("handle")
rkey := c.Param("rkey")
// sanity check argument
if len(handle) > 4 && len(handle) < 128 && len(rkey) > 0 {
ctx := c.Request().Context()
// requires two fetches: first fetch profile (!)
pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle)
if err != nil {
log.Warnf("failed to fetch handle: %s\t%v", handle, err)
} else {
did := pv.Did
data["did"] = did
// then fetch the post thread (with extra context)
uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey)
tpv, err := appbsky.FeedGetPostThread(ctx, srv.xrpcc, 1, uri)
if err != nil {
log.Warnf("failed to fetch post: %s\t%v", uri, err)
} else {
req := c.Request()
postView := tpv.Thread.FeedDefs_ThreadViewPost.Post
data["postView"] = postView
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
if postView.Embed != nil && postView.Embed.EmbedImages_View != nil {
data["imgThumbUrl"] = postView.Embed.EmbedImages_View.Images[0].Thumb
}
}
// sanity check arguments. don't 4xx, just let app handle if not expected format
rkeyParam := c.Param("rkey")
rkey, err := syntax.ParseRecordKey(rkeyParam)
if err != nil {
return c.Render(http.StatusOK, "post.html", data)
}
handleParam := c.Param("handle")
handle, err := syntax.ParseHandle(handleParam)
if err != nil {
return c.Render(http.StatusOK, "post.html", data)
}
handle = handle.Normalize()
// requires two fetches: first fetch profile (!)
pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle.String())
if err != nil {
log.Warnf("failed to fetch handle: %s\t%v", handle, err)
return c.Render(http.StatusOK, "post.html", data)
}
unauthedViewingOkay := true
for _, label := range pv.Labels {
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
unauthedViewingOkay = false
}
}
if !unauthedViewingOkay {
return c.Render(http.StatusOK, "post.html", data)
}
did := pv.Did
data["did"] = did
// then fetch the post thread (with extra context)
uri := fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey)
tpv, err := appbsky.FeedGetPostThread(ctx, srv.xrpcc, 1, 0, uri)
if err != nil {
log.Warnf("failed to fetch post: %s\t%v", uri, err)
return c.Render(http.StatusOK, "post.html", data)
}
req := c.Request()
postView := tpv.Thread.FeedDefs_ThreadViewPost.Post
data["postView"] = postView
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
if postView.Embed != nil && postView.Embed.EmbedImages_View != nil {
data["imgThumbUrl"] = postView.Embed.EmbedImages_View.Images[0].Thumb
}
return c.Render(http.StatusOK, "post.html", data)
}
func (srv *Server) WebProfile(c echo.Context) error {
ctx := c.Request().Context()
data := pongo2.Context{}
handle := c.Param("handle")
// sanity check argument
if len(handle) > 4 && len(handle) < 128 {
ctx := c.Request().Context()
pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle)
if err != nil {
log.Warnf("failed to fetch handle: %s\t%v", handle, err)
} else {
req := c.Request()
data["profileView"] = pv
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
// sanity check arguments. don't 4xx, just let app handle if not expected format
handleParam := c.Param("handle")
handle, err := syntax.ParseHandle(handleParam)
if err != nil {
return c.Render(http.StatusOK, "profile.html", data)
}
handle = handle.Normalize()
pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle.String())
if err != nil {
log.Warnf("failed to fetch handle: %s\t%v", handle, err)
return c.Render(http.StatusOK, "profile.html", data)
}
unauthedViewingOkay := true
for _, label := range pv.Labels {
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
unauthedViewingOkay = false
}
}
if !unauthedViewingOkay {
return c.Render(http.StatusOK, "profile.html", data)
}
req := c.Request()
data["profileView"] = pv
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
return c.Render(http.StatusOK, "profile.html", data)
}
+1 -1
View File
@@ -1,2 +1,2 @@
GOLOG_LOG_LEVEL=info
ATP_APPVIEW_HOST=https://api.bsky.app
ATP_APPVIEW_HOST=https://public.api.bsky.app
+55 -39
View File
@@ -3,88 +3,104 @@ module github.com/bluesky-social/social-app/bskyweb
go 1.21
require (
github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319
github.com/bluesky-social/indigo v0.0.0-20231216010655-ad730a7da4f5
github.com/flosch/pongo2/v6 v6.0.0
github.com/ipfs/go-log v1.0.5
github.com/joho/godotenv v1.5.1
github.com/klauspost/compress v1.16.5
github.com/labstack/echo/v4 v4.10.2
github.com/urfave/cli/v2 v2.25.3
github.com/klauspost/compress v1.17.3
github.com/labstack/echo/v4 v4.11.3
github.com/urfave/cli/v2 v2.25.7
)
require (
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/carlmjohnson/versioninfo v0.22.5 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/arc/v2 v2.0.6 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/ipfs/bbloom v0.0.4 // indirect
github.com/ipfs/go-block-format v0.1.2 // indirect
github.com/ipfs/go-block-format v0.2.0 // indirect
github.com/ipfs/go-cid v0.4.1 // indirect
github.com/ipfs/go-datastore v0.6.0 // indirect
github.com/ipfs/go-ipfs-blockstore v1.3.0 // indirect
github.com/ipfs/go-ipfs-ds-help v1.1.0 // indirect
github.com/ipfs/go-ipfs-util v0.0.2 // indirect
github.com/ipfs/go-ipld-cbor v0.0.7-0.20230126201833-a73d038d90bc // indirect
github.com/ipfs/go-ipld-format v0.4.0 // indirect
github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect
github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect
github.com/ipfs/go-ipfs-util v0.0.3 // indirect
github.com/ipfs/go-ipld-cbor v0.1.0 // indirect
github.com/ipfs/go-ipld-format v0.6.0 // indirect
github.com/ipfs/go-log/v2 v2.5.1 // indirect
github.com/ipfs/go-metrics-interface v0.0.1 // indirect
github.com/ipsn/go-secp256k1 v0.0.0-20180726113642-9d62b9f0bc52 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.3.1 // indirect
github.com/jackc/pgx/v5 v5.5.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jbenet/goprocess v0.1.4 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/labstack/gommon v0.4.1 // indirect
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/jwx/v2 v2.0.9 // indirect
github.com/lestrrat-go/jwx/v2 v2.0.12 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.18 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/multiformats/go-multihash v0.2.1 // indirect
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
github.com/prometheus/client_golang v1.17.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/whyrusleeping/cbor-gen v0.0.0-20230418232409-daab9ece03a0 // indirect
github.com/whyrusleeping/go-did v0.0.0-20230301193428-2146016fc220 // indirect
github.com/whyrusleeping/cbor-gen v0.0.0-20230923211252-36a87e1ba72f // indirect
github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.opentelemetry.io/otel v1.15.1 // indirect
go.opentelemetry.io/otel/trace v1.15.1 // indirect
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.15.0 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gorm.io/driver/postgres v1.5.0 // indirect
gorm.io/driver/sqlite v1.5.0 // indirect
gorm.io/gorm v1.25.0 // indirect
lukechampine.com/blake3 v1.1.7 // indirect
gorm.io/driver/sqlite v1.5.4 // indirect
gorm.io/gorm v1.25.5 // indirect
lukechampine.com/blake3 v1.2.1 // indirect
)
+132 -116
View File
@@ -1,25 +1,30 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319 h1:VCNXRXpgyK3xkaQ8fzL5WzswerwLycke4B9ggLs1uOA=
github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319/go.mod h1:Hc09SUJXAIujaAvq7JXxi8ZQQI887grzPkHgn4JyE1Q=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bluesky-social/indigo v0.0.0-20231216010655-ad730a7da4f5 h1:Zk1c+mxCYH6G/vLL0+9lO2Eci4OT3AFy73qPWa9auDM=
github.com/bluesky-social/indigo v0.0.0-20231216010655-ad730a7da4f5/go.mod h1:a8cPbqDkRX+aPwJnXF7kAi3PF26hYiR4w5H8624MB7k=
github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU=
github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
@@ -29,53 +34,49 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gxed/hashland/keccakpg v0.0.1/go.mod h1:kRzw3HkwxFU1mpmPP8v1WyQzwdGfmKFJ6tItnhQ67kU=
github.com/gxed/hashland/murmur3 v0.0.1/go.mod h1:KjXop02n4/ckmZSnY2+HKcLud/tcmvhST0bie/0lS48=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/arc/v2 v2.0.6 h1:4NU7uP5vSoK6TbaMj3NtY478TTAWLso/vL1gpNrInHg=
github.com/hashicorp/golang-lru/arc/v2 v2.0.6/go.mod h1:cfdDIX05DWvYV6/shsxDfa/OVcRieOt+q4FnM8x+Xno=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
github.com/ipfs/go-block-format v0.0.2/go.mod h1:AWR46JfpcObNfg3ok2JHDUfdiHRgWhJgCQF+KIgOPJY=
github.com/ipfs/go-block-format v0.1.2 h1:GAjkfhVx1f4YTODS6Esrj1wt2HhrtwTnhEr+DyPUaJo=
github.com/ipfs/go-block-format v0.1.2/go.mod h1:mACVcrxarQKstUU3Yf/RdwbC4DzPV6++rO2a3d+a/KE=
github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
github.com/ipfs/go-cid v0.0.2/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM=
github.com/ipfs/go-cid v0.0.5/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67FexhXog=
github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs=
github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I=
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
github.com/ipfs/go-datastore v0.5.0/go.mod h1:9zhEApYMTl17C8YDp7JmU7sQZi2/wqiYh73hakZ90Bk=
github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8=
github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk=
github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps=
github.com/ipfs/go-ipfs-blockstore v1.3.0 h1:m2EXaWgwTzAfsmt5UdJ7Is6l4gJcaM/A12XwJyvYvMM=
github.com/ipfs/go-ipfs-blockstore v1.3.0/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE=
github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw=
github.com/ipfs/go-ipfs-ds-help v1.1.0 h1:yLE2w9RAsl31LtfMt91tRZcrx+e61O5mDxFRR994w4Q=
github.com/ipfs/go-ipfs-ds-help v1.1.0/go.mod h1:YR5+6EaebOhfcqVCyqemItCLthrpVNot+rsOU/5IatU=
github.com/ipfs/go-ipfs-util v0.0.1/go.mod h1:spsl5z8KUnrve+73pOhSVZND1SIxPW5RyBCNzQxlJBc=
github.com/ipfs/go-ipfs-util v0.0.2 h1:59Sswnk1MFaiq+VcaknX7aYEyGyGDAA73ilhEK2POp8=
github.com/ipfs/go-ipfs-util v0.0.2/go.mod h1:CbPtkWJzjLdEcezDns2XYaehFVNXG9zrdrtMecczcsQ=
github.com/ipfs/go-ipld-cbor v0.0.7-0.20230126201833-a73d038d90bc h1:eUEo764smNy0EVRuMTSmirmuh552Mf2aBjfpDcLnDa8=
github.com/ipfs/go-ipld-cbor v0.0.7-0.20230126201833-a73d038d90bc/go.mod h1:X7SgEIwC4COC5OWfcepZBWafO5kA1Rmt9ZsLLbhihQk=
github.com/ipfs/go-ipld-format v0.4.0 h1:yqJSaJftjmjc9jEOFYlpkwOLVKv68OD27jFLlSghBlQ=
github.com/ipfs/go-ipld-format v0.4.0/go.mod h1:co/SdBE8h99968X0hViiw1MNlh6fvxxnHpvVLnH7jSM=
github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ=
github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE=
github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw=
github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo=
github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0=
github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs=
github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs=
github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk=
github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U=
github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg=
github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8=
github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo=
github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g=
@@ -83,16 +84,16 @@ github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY=
github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI=
github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg=
github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY=
github.com/ipsn/go-secp256k1 v0.0.0-20180726113642-9d62b9f0bc52 h1:QG4CGBqCeuBo6aZlGAamSkxWdgWfZGeE49eUOWJPA4c=
github.com/ipsn/go-secp256k1 v0.0.0-20180726113642-9d62b9f0bc52/go.mod h1:fdg+/X9Gg4AsAIzWpEHwnqd+QY3b7lajxyjE1m4hkq4=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw=
github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA=
github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o=
github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
@@ -106,25 +107,23 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA=
github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM=
github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws=
github.com/labstack/gommon v0.4.1 h1:gqEff0p/hTENGMABzezPoPSRtIh1Cvw0ueMOe0/dfOk=
github.com/labstack/gommon v0.4.1/go.mod h1:TyTrpPqxR5KMk8LKVtLmfMjeQ5FEkBYdxLYPw/WfrOM=
github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
@@ -133,26 +132,25 @@ github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJG
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx/v2 v2.0.9 h1:TRX4Q630UXxPVLvP5vGaqVJO7S+0PE6msRZUsFSBoC8=
github.com/lestrrat-go/jwx/v2 v2.0.9/go.mod h1:K68euYaR95FnL0hIQB8VvzL70vB7pSifbJUydCTPmgM=
github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA=
github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=
github.com/minio/sha256-simd v0.0.0-20190131020904-2d45a736cd16/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U=
github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
@@ -165,32 +163,39 @@ github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYg
github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM=
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
github.com/multiformats/go-multibase v0.0.1/go.mod h1:bja2MqRZ3ggyXtZSEDKpl0uO/gviWFaSteVbWT51qgs=
github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc=
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
github.com/multiformats/go-multihash v0.0.1/go.mod h1:w/5tugSrLEbWqlcgJabL3oHFKTwfvkofsjW2Qa1ct4U=
github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc=
github.com/multiformats/go-multihash v0.2.1 h1:aem8ZT0VA2nCHHk7bPJ1BjUbHNciqZC/d16Vve9l108=
github.com/multiformats/go-multihash v0.2.1/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc=
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
@@ -209,39 +214,46 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY=
github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
github.com/whyrusleeping/cbor-gen v0.0.0-20230418232409-daab9ece03a0 h1:XYEgH2nJgsrcrj32p+SAbx6T3s/6QknOXezXtz7kzbg=
github.com/whyrusleeping/cbor-gen v0.0.0-20230418232409-daab9ece03a0/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ=
github.com/whyrusleeping/go-did v0.0.0-20230301193428-2146016fc220 h1:EO/9z3yDvx1van1/0esdcqhalZZQGRj3I1BPTWr5k3A=
github.com/whyrusleeping/go-did v0.0.0-20230301193428-2146016fc220/go.mod h1:qPtRyexGM5XMHFIfjH+EiA/A/1n2JakWEdMPC53pJAE=
github.com/whyrusleeping/cbor-gen v0.0.0-20230923211252-36a87e1ba72f h1:SBuSxXJL0/ZJMtTxbXZgHZkThl9dNrzyaNhlyaqscRo=
github.com/whyrusleeping/cbor-gen v0.0.0-20230923211252-36a87e1ba72f/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ=
github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6 h1:yJ9/LwIGIk/c0CdoavpC9RNSGSruIspSZtxG3Nnldic=
github.com/whyrusleeping/go-did v0.0.0-20230824162731-404d1707d5d6/go.mod h1:39U9RRVr4CKbXpXYopWn+FSH5s+vWu6+RmguSPWAq5s=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/otel v1.15.1 h1:3Iwq3lfRByPaws0f6bU3naAqOR1n5IeDWd9390kWHa8=
go.opentelemetry.io/otel v1.15.1/go.mod h1:mHHGEHVDLal6YrKMmk9LqC4a3sF5g+fHfrttQIB1NTc=
go.opentelemetry.io/otel/trace v1.15.1 h1:uXLo6iHJEzDfrNC0L0mNjItIp06SyaBQxu5t3xMlngY=
go.opentelemetry.io/otel/trace v1.15.1/go.mod h1:IWdQG/5N1x7f6YUlmdLeJvH9yxtuJAfc4VW5Agv9r/8=
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA=
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0/go.mod h1:XiYsayHc36K3EByOO6nbAXnAWbrUxdjUROCEeeROOH8=
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -249,9 +261,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -259,9 +270,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -278,17 +289,18 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -296,27 +308,29 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -335,11 +349,13 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
@@ -351,11 +367,11 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c=
gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I=
gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
+3 -2
View File
@@ -71,6 +71,7 @@
overflow-y: auto;
overscroll-behavior-y: none;
text-rendering: optimizeLegibility;
background-color: var(--background);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-ms-overflow-style: scrollbar;
@@ -176,8 +177,8 @@
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
{% block html_head_extra -%}{%- endblock %}
<meta name="application-name" name="Bluesky">
<meta name="generator" name="bskyweb">
<meta name="application-name" content="Bluesky">
<meta name="generator" content="bskyweb">
</head>
<body>
{%- block body_all %}
+1
View File
@@ -8,6 +8,7 @@
<meta property="og:title" content="Bluesky Social">
<meta property="og:description" content="See what's next.">
<meta property="og:image" content="/static/social-card-default.png">
<meta property="og:site_name" content="Bluesky Social">
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@bluesky">
{%- endblock %}
+7 -3
View File
@@ -10,8 +10,9 @@
{% block html_head_extra -%}
{%- if postView -%}
<meta property="og:type" content="website">
<meta property="og:site_name" content="Bluesky Social">
<meta property="og:type" content="article">
<meta property="profile:username" content="{{ profileView.Handle }}">
{%- if requestURI %}
<meta property="og:url" content="{{ requestURI }}">
{% endif -%}
@@ -32,17 +33,20 @@
<meta name="twitter:card" content="summary">
{% endif %}
<meta name="twitter:label1" content="Posted At">
<meta name="twitter:value1" content="{{ postView.CreatedAt }}">
<meta name="twitter:site" content="@bluesky">
<meta name="twitter:value1" content="{{ postView.IndexedAt }}">
<meta name="article:published_time" content="{{ postView.IndexedAt }}">
{% endif -%}
{%- endblock %}
{% block noscript_extra -%}
{%- if postView -%}
<div id="bsky_post_summary">
<h3>Post</h3>
<p id="bsky_display_name">{{ postView.Author.DisplayName }}</p>
<p id="bsky_handle">{{ postView.Author.Handle }}</p>
<p id="bsky_did">{{ postView.Author.Did }}</p>
<p id="bsky_post_text">{{ postView.Record.Val.Text }}</p>
<p id="bsky_post_indexedat">{{ postView.IndexedAt }}</p>
</div>
{% endif -%}
{%- endblock %}
+5 -2
View File
@@ -10,8 +10,9 @@
{% block html_head_extra -%}
{%- if profileView -%}
<meta property="og:type" content="website">
<meta property="og:site_name" content="Bluesky Social">
<meta property="og:type" content="profile">
<meta property="profile:username" content="{{ profileView.Handle }}">
{%- if requestURI %}
<meta property="og:url" content="{{ requestURI }}">
{% endif -%}
@@ -33,11 +34,12 @@
{% endif %}
<meta name="twitter:label1" content="Account DID">
<meta name="twitter:value1" content="{{ profileView.Did }}">
<meta name="twitter:site" content="@bluesky">
<link rel="alternate" type="application/rss+xml" href="/profile/{{ profileView.Did }}/rss">
{% endif -%}
{%- endblock %}
{% block noscript_extra -%}
{%- if profileView -%}
<div id="bsky_profile_summary">
<h3>Profile</h3>
<p id="bsky_display_name">{{ profileView.DisplayName }}</p>
@@ -45,4 +47,5 @@
<p id="bsky_did">{{ profileView.Did }}</p>
<p id="bsky_profile_description">{{ profileView.Description }}</p>
</div>
{% endif -%}
{%- endblock %}
+15 -14
View File
@@ -2,7 +2,7 @@
## App Build
- Setup your environment [using the react native instructions](https://reactnative.dev/docs/environment-setup).
- Set up your environment [using the react native instructions](https://reactnative.dev/docs/environment-setup).
- Setup your environment [for e2e testing using detox](https://wix.github.io/Detox/docs/introduction/getting-started):
- yarn global add detox-cli
- brew tap wix/brew
@@ -10,6 +10,7 @@
- After initial setup:
- Copy `google-services.json.example` to `google-services.json` or provide your own `google-services.json`. (A real firebase project is NOT required)
- `npx expo prebuild` -> you will also need to run this anytime `app.json` or native `package.json` deps change
- `yarn intl:build` -> you will also need to run this anytime `./src/locale/{locale}/messages.po` change
- Start the dev servers
- `git clone git@github.com:bluesky-social/atproto.git`
- `cd atproto`
@@ -20,7 +21,7 @@
- iOS: `yarn ios`
- Android: `yarn android`
- Web: `yarn web`
- If you are cloning or forking this repo as an open source developer, please check the tips below as well
- If you are cloning or forking this repo as an open-source developer, please check the tips below as well
- Run e2e tests
- Start in various console tabs:
- `yarn e2e:mock-server`
@@ -28,14 +29,14 @@
- Run once: `yarn e2e:build`
- Each test run: `yarn e2e:run`
- Tips
- Make sure to copy the `.env.example` to `.env` and fill in any necessary tokens. (The Sentry token is NOT required; see instructions below if you want to enable Sentry.)
- To run on device, add `--device` to the command (e.g. `yarn android --device`). To build in production mode (slower build, faster app), also add `--variant release`.
- If you want to use Expo EAS on your own builds without ejecting from Expo, make sure to change the `owner` as well as `extra.eas.projectId` properties. If you do not have an Expo account, you may remove these properties.
- Copy the `.env.example` to `.env` and fill in any necessary tokens. (The Sentry token is NOT required; see instructions below if you want to enable Sentry.)
- To run on the device, add `--device` to the command (e.g. `yarn android --device`). To build in production mode (slower build, faster app), also add `--variant release`.
- If you want to use Expo EAS on your own builds without ejecting from Expo, make sure to change the `owner` and `extra.eas.projectId` properties. If you do not have an Expo account, you may remove these properties.
- `npx react-native info` Checks what has been installed.
- If the android simulator frequently hangs or is very sluggish, [bump its memory limit](https://stackoverflow.com/a/40068396)
- The android simulator won't be able to access localhost services unless you run `adb reverse tcp:{PORT} tcp:{PORT}`
- If the Android simulator frequently hangs or is very sluggish, [bump its memory limit](https://stackoverflow.com/a/40068396)
- The Android simulator won't be able to access localhost services unless you run `adb reverse tcp:{PORT} tcp:{PORT}`
- For instance, the locally-hosted dev-wallet will need `adb reverse tcp:3001 tcp:3001`
- For some reason, the typescript compiler chokes on platform-specific files (e.g. `foo.native.ts`) but only when compiling for Web thus far. Therefore we always have one version of the file which doesn't use a platform specifier, and that should be the Web version. ([More info](https://stackoverflow.com/questions/44001050/platform-specific-import-component-in-react-native-with-typescript).)
- For some reason, the typescript compiler chokes on platform-specific files (e.g. `foo.native.ts`) but only when compiling for Web thus far. Therefore we always have one version of the file that doesn't use a platform specifier, and that should be the Web version. ([More info](https://stackoverflow.com/questions/44001050/platform-specific-import-component-in-react-native-with-typescript).)
### Adding Sentry
@@ -81,24 +82,24 @@ To open the [Developer Menu](https://docs.expo.dev/debugging/tools/#developer-me
- Android Device: Shake the device vertically, or if your device is connected via USB, run adb shell input keyevent 82 in your terminal
- Android Emulator: Either press Cmd ⌘ + m or Ctrl + m or run adb shell input keyevent 82 in your terminal
- iOS Device: Shake the device, or touch 3 fingers to the screen
- iOS Simulator: Press Ctrl + Cmd ⌘ + z on a Mac in the emulator to simulate the shake gesture, or press Cmd ⌘ + d
- iOS Simulator: Press Ctrl + Cmd ⌘ + z on a Mac in the emulator to simulate the shake gesture or press Cmd ⌘ + d
### Running E2E Tests
- Make sure you've setup your environment following above
- Make sure you've set your environment following the above
- Make sure Metro and the dev server are running
- Run `yarn e2e`
- Find the artifacts in the `artifact` folder
### Polyfills
`./platform/polyfills.*.ts` adds polyfills to the environment. Currently this includes:
`./platform/polyfills.*.ts` adds polyfills to the environment. Currently, this includes:
- TextEncoder / TextDecoder
### Sentry sourcemaps
Sourcemaps should automatically be updated when a signed build is created using `eas build` and published using `eas submit` due to the postPublish hook setup in `app.json`. However, if an update is created and published OTA using `eas update`, we need to the take the following steps to upload sourcemaps to Sentry:
Sourcemaps should automatically be updated when a signed build is created using `eas build` and published using `eas submit` due to the postPublish hook setup in `app.json`. However, if an update is created and published OTA using `eas update`, we need to take the following steps to upload sourcemaps to Sentry:
- Run eas update. This will generate a dist folder in your project root, which contains your JavaScript bundles and source maps. This command will also output the 'Android update ID' and 'iOS update ID' that we'll need in the next step.
- Copy or rename the bundle names in the `dist/bundles` folder to match `index.android.bundle` (Android) or `main.jsbundle` (iOS).
@@ -122,6 +123,6 @@ dist/bundles/main.jsbundle dist/bundles/ios-<hash>.map`
### OTA updates
To create OTA updates, run `eas update` along with the `--branch` flag to indicate which branch you want to push the update to, and the `--message` flag to indicate a message for yourself and your team that shows up on https://expo.dev. ALl the channels (which make up the options for the `--branch` flag) are given in `eas.json`. [See more here](https://docs.expo.dev/eas-update/getting-started/)
To create OTA updates, run `eas update` along with the `--branch` flag to indicate which branch you want to push the update to, and the `--message` flag to indicate a message for yourself and your team that shows up on https://expo.dev. All the channels (which make up the options for the `--branch` flag) are given in `eas.json`. [See more here](https://docs.expo.dev/eas-update/getting-started/)
The clients which can receive an OTA update is governed by the `runtimeVersion` property in `app.json`. Right now, it is set so that only apps with the same `appVersion` (same as `version` property in `app.json`) can receive the update and install it. However, we can manually set `"runtimeVersion": "1.34.0"` or anything along those lines as well. This is useful if very little native code changes from update-to-update. If we are manually setting `runtimeVersion`, we should increment the version each time native code is changed. [See more here](https://docs.expo.dev/eas-update/runtime-versions/)
The clients which can receive an OTA update are governed by the `runtimeVersion` property in `app.json`. Right now, it is set so that only apps with the same `appVersion` (same as `version` property in `app.json`) can receive the update and install it. However, we can manually set `"runtimeVersion": "1.34.0"` or anything along those lines as well. This is useful if very little native code changes from update to update. If we are manually setting `runtimeVersion`, we should increment the version each time the native code is changed. [See more here](https://docs.expo.dev/eas-update/runtime-versions/)
+119
View File
@@ -0,0 +1,119 @@
# Internationalization
We want the official Bluesky app to be supported in as many languages as possible. If you want to help us translate the app, please open a PR or issue on the [Bluesky app repo on GitHub](https://github.com/bluesky-social/social-app)
## Tools
We are using Lingui to manage translations. You can find the documentation [here](https://lingui.dev/).
### Adding new strings
When adding a new string, do it as follows:
```jsx
// Before
import { Text } from "react-native";
<Text>Hello World</Text>
```
```jsx
// After
import { Text } from "react-native";
import { Trans } from "@lingui/macro";
<Text><Trans>Hello World</Trans></Text>
```
The `<Trans>` macro will extract the string and add it to the catalog. It is not really a component, but a macro. Further reading [here](https://lingui.dev/ref/macro.html)
However sometimes you will run into this case:
```jsx
// Before
import { Text } from "react-native";
const text = "Hello World";
<Text accessibilityLabel="Label is here">{text}</Text>
```
In this case, you cannot use the `useLingui()` hook:
```jsx
import { msg } from "@lingui/macro";
import { useLingui } from "@lingui/react";
const { _ } = useLingui();
return <Text accessibilityLabel={_(msg`Label is here`)}>{text}</Text>
```
If you want to do this outside of a React component, you can use the `t` macro instead (note: this won't react to changes if the locale is switched dynamically within the app):
```jsx
import { t } from "@lingui/macro";
const text = t`Hello World`;
```
We can then run `yarn intl:extract` to update the catalog in `src/locale/locales/{locale}/messages.po`. This will add the new string to the catalog.
We can then run `yarn intl:compile` to update the translation files in `src/locale/locales/{locale}/messages.js`. This will add the new string to the translation files.
The configuration for translations is defined in `lingui.config.js`
So the workflow is as follows:
1. Wrap messages in Trans macro
2. Run `yarn intl:extract` command to generate message catalogs
3. Translate message catalogs (send them to translators usually)
4. Run `yarn intl:compile` to create runtime catalogs
5. Load runtime catalog
6. Enjoy translated app!
### Common pitfalls
These pitfalls are memoization pitfalls that will cause the components to not re-render when the locale is changed -- causing stale translations to be shown.
```jsx
import { msg } from "@lingui/macro";
import { i18n } from "@lingui/core";
const welcomeMessage = msg`Welcome!`;
// ❌ Bad! This code won't work
export function Welcome() {
const buggyWelcome = useMemo(() => {
return i18n._(welcomeMessage);
}, []);
return <div>{buggyWelcome}</div>;
}
// ❌ Bad! This code won't work either because the reference to i18n does not change
export function Welcome() {
const { i18n } = useLingui();
const buggyWelcome = useMemo(() => {
return i18n._(welcomeMessage);
}, [i18n]);
return <div>{buggyWelcome}</div>;
}
// ✅ Good! `useMemo` has i18n context in the dependency
export function Welcome() {
const linguiCtx = useLingui();
const welcome = useMemo(() => {
return linguiCtx.i18n._(welcomeMessage);
}, [linguiCtx]);
return <div>{welcome}</div>;
}
// 🤩 Better! `useMemo` consumes the `_` function from the Lingui context
export function Welcome() {
const { _ } = useLingui();
const welcome = useMemo(() => {
return _(welcomeMessage);
}, [_]);
return <div>{welcome}</div>;
}
```
### Credits
Please check each individual `messages.po` file for the credits of the translators. We are very grateful for their help!
If you would like to translate the Bluesky app into your language, please open a PR or issue on this repo.
+4 -1
View File
@@ -1,5 +1,8 @@
# Testing instructions
Make sure you've copied `.env.example` to `.env.test` and provided any required
values.
### Using Maestro E2E tests
1. Install Maestro by following [these instructions](https://maestro.mobile.dev/getting-started/installing-maestro). This will help us run the E2E tests.
2. You can write Maestro tests in `__e2e__/maestro` directory by creating a new `.yaml` file or by modifying an existing one.
@@ -11,4 +14,4 @@
2. Install Flashlight by following [these instructions](https://docs.flashlight.dev/)
3. The simplest way to get started is by running `yarn perf:measure` which will run a live preview of the performance test results. You can [see a demo here](https://github.com/bamlab/flashlight/assets/4534323/4038a342-f145-4c3b-8cde-17949bf52612)
4. The `yarn perf:test:measure` will run the `scroll.yaml` test located in `__e2e__/maestro/scroll.yaml` and give the results in `.perf/results.json` which can be viewed by running `yarn:perf:results`
5. You can also run your own tests by running `yarn perf:test <path_to_test>` where `<path_to_test>` is the path to your test file. For example, `yarn perf:test __e2e__/maestro/scroll.yaml` will run the `scroll.yaml` test located in `__e2e__/maestro/scroll.yaml`.
5. You can also run your own tests by running `yarn perf:test <path_to_test>` where `<path_to_test>` is the path to your test file. For example, `yarn perf:test __e2e__/maestro/scroll.yaml` will run the `scroll.yaml` test located in `__e2e__/maestro/scroll.yaml`.
+13 -22
View File
@@ -4,42 +4,33 @@
"promptToConfigurePushNotifications": false
},
"build": {
"base": {
"node": "18.18.2"
},
"development": {
"extends": "base",
"developmentClient": true,
"distribution": "internal",
"channel": "development",
"ios": {
"simulator": true,
"resourceClass": "m-large"
},
"channel": "development"
},
"development-device": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"resourceClass": "m-large"
},
"channel": "development"
"resourceClass": "large"
}
},
"preview": {
"extends": "base",
"distribution": "internal",
"channel": "preview",
"ios": {
"resourceClass": "m-large"
},
"channel": "preview"
"resourceClass": "large"
}
},
"production": {
"extends": "base",
"ios": {
"resourceClass": "m-large"
"resourceClass": "large"
},
"channel": "production"
},
"dev-android-apk": {
"developmentClient": true,
"android": {
"buildType": "apk",
"gradleCommand": ":app:assembleRelease"
}
}
},
"submit": {
+12 -4
View File
@@ -1,12 +1,20 @@
import 'react-native-gesture-handler' // must be first
import {LogBox} from 'react-native'
LogBox.ignoreLogs(['Require cycle:']) // suppress require-cycle warnings, it's fine
import 'platform/polyfills'
import '#/platform/polyfills'
import {IS_TEST} from '#/env'
import {registerRootComponent} from 'expo'
import {doPolyfill} from '#/lib/api/api-polyfill'
import App from './src/App'
import App from '#/App'
doPolyfill()
if (IS_TEST) {
LogBox.ignoreAllLogs() // suppress all logs in tests
} else {
LogBox.ignoreLogs(['Require cycle:']) // suppress require-cycle warnings, it's fine
}
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
+5 -2
View File
@@ -1,4 +1,7 @@
import 'platform/polyfills'
import '#/platform/polyfills'
import {registerRootComponent} from 'expo'
import App from './src/App'
import {doPolyfill} from '#/lib/api/api-polyfill'
import App from '#/App'
doPolyfill()
registerRootComponent(App)
+3
View File
@@ -2,6 +2,9 @@
import {configure} from '@testing-library/react-native'
import 'react-native-gesture-handler/jestSetup'
// IMPORTANT: this is what's used in the native runtime
import 'react-native-url-polyfill/auto'
configure({asyncUtilTimeout: 20000})
jest.mock('@react-native-async-storage/async-storage', () =>
+6 -3
View File
@@ -59,23 +59,26 @@ export async function createServer(
): Promise<TestPDS> {
const port = await getPort()
const port2 = await getPort(port + 1)
const port3 = await getPort(port2 + 1)
const pdsUrl = `http://localhost:${port}`
const id = ids.next()
const testNet = await TestNetwork.create({
pds: {
port,
publicUrl: pdsUrl,
hostname: 'localhost',
inviteRequired,
dbPostgresSchema: `pds_${id}`,
},
bsky: {
dbPostgresSchema: `bsky_${id}`,
port: port3,
publicUrl: 'http://localhost:2584',
},
plc: {port: port2},
})
const pic = fs.readFileSync(
path.join(__dirname, '..', 'assets', 'default-avatar.jpg'),
path.join(__dirname, '..', 'assets', 'default-avatar.png'),
)
return {
+11
View File
@@ -0,0 +1,11 @@
/** @type {import('@lingui/conf').LinguiConfig} */
module.exports = {
locales: ['en', 'hi', 'ja'],
catalogs: [
{
path: '<rootDir>/src/locale/locales/{locale}/messages',
include: ['src'],
},
],
format: 'po',
}
+37 -21
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.55.0",
"version": "1.60.0",
"private": true,
"scripts": {
"prepare": "is-ci || husky install",
@@ -10,28 +10,32 @@
"ios": "expo run:ios",
"web": "expo start --web",
"build-web": "expo export:web && node ./scripts/post-web-build.js && cp --verbose ./web-build/static/js/*.* ./bskyweb/static/js/",
"build-all": "yarn intl:build && eas build --platform all",
"start": "expo start --dev-client",
"start:prod": "expo start --dev-client --no-dev --minify",
"clean-cache": "rm -rf node_modules/.cache/babel-loader/*",
"test": "jest --forceExit --testTimeout=20000 --bail",
"test-watch": "jest --watchAll",
"test-ci": "jest --ci --forceExit --reporters=default --reporters=jest-junit",
"test-coverage": "jest --coverage",
"test": "NODE_ENV=test jest --forceExit --testTimeout=20000 --bail",
"test-watch": "NODE_ENV=test jest --watchAll",
"test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit",
"test-coverage": "NODE_ENV=test jest --coverage",
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx",
"typecheck": "tsc --project ./tsconfig.check.json",
"e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node __e2e__/mock-server.ts",
"e2e:metro": "RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
"e2e:build": "detox build -c ios.sim.debug",
"e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all",
"perf:test": "maestro test",
"perf:test:run": "maestro test __e2e__/maestro/scroll.yaml",
"perf:test:measure": "flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json",
"perf:test:results": "flashlight report .perf/results.json",
"perf:measure": "flashlight measure",
"build:apk": "eas build -p android --profile dev-android-apk"
"e2e:metro": "NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
"e2e:build": "NODE_ENV=test detox build -c ios.sim.debug",
"e2e:run": "NODE_ENV=test detox test --configuration ios.sim.debug --take-screenshots all",
"perf:test": "NODE_ENV=test maestro test",
"perf:test:run": "NODE_ENV=test maestro test __e2e__/maestro/scroll.yaml",
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json",
"perf:test:results": "NODE_ENV=test flashlight report .perf/results.json",
"perf:measure": "NODE_ENV=test flashlight measure",
"intl:build": "yarn intl:check && yarn intl:compile",
"intl:check": "yarn intl:extract && git diff-index -G'(^[^\\*# /])|(^#\\w)|(^\\s+[^\\*#/])' HEAD || (echo '\n⚠️ i18n detected un-extracted translations\n' && exit 1)",
"intl:extract": "lingui extract",
"intl:compile": "lingui compile"
},
"dependencies": {
"@atproto/api": "^0.6.21",
"@atproto/api": "^0.7.4",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@emoji-mart/react": "^1.1.1",
@@ -42,6 +46,7 @@
"@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",
"@react-native-async-storage/async-storage": "1.18.2",
@@ -49,6 +54,7 @@
"@react-native-clipboard/clipboard": "^1.10.0",
"@react-native-community/blur": "^4.3.0",
"@react-native-community/datetimepicker": "7.2.0",
"@react-native-masked-view/masked-view": "^0.3.1",
"@react-native-menu/menu": "^0.8.0",
"@react-native-picker/picker": "2.4.10",
"@react-navigation/bottom-tabs": "^6.5.7",
@@ -60,7 +66,7 @@
"@segment/analytics-react-native": "^2.10.1",
"@segment/sovran-react-native": "^0.4.5",
"@sentry/react-native": "5.10.0",
"@tanstack/react-query": "^4.33.0",
"@tanstack/react-query": "^5.8.1",
"@tiptap/core": "^2.0.0-beta.220",
"@tiptap/extension-document": "^2.0.0-beta.220",
"@tiptap/extension-hard-break": "^2.0.3",
@@ -80,6 +86,7 @@
"babel-plugin-transform-remove-console": "^6.9.4",
"base64-js": "^1.5.1",
"bcp-47-match": "^2.0.3",
"date-fns": "^2.30.0",
"email-validator": "^2.0.4",
"emoji-mart": "^5.5.2",
"eventemitter3": "^5.0.1",
@@ -104,6 +111,7 @@
"fast-text-encoding": "^1.0.6",
"history": "^5.3.0",
"js-sha256": "^0.9.0",
"jwt-decode": "^4.0.0",
"lande": "^1.0.10",
"lodash.chunk": "^4.2.0",
"lodash.debounce": "^4.0.8",
@@ -118,6 +126,7 @@
"mobx": "^6.6.1",
"mobx-react-lite": "^3.4.0",
"mobx-utils": "^6.0.6",
"nanoid": "^5.0.2",
"normalize-url": "^8.0.0",
"patch-package": "^6.5.1",
"postinstall-postinstall": "^2.1.0",
@@ -128,7 +137,6 @@
"react-dom": "^18.2.0",
"react-native": "0.72.5",
"react-native-appstate-hook": "^1.0.6",
"react-native-draggable-flatlist": "^4.0.1",
"react-native-drawer-layout": "^3.2.0",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "^2.12.1",
@@ -141,7 +149,7 @@
"react-native-pager-view": "6.1.4",
"react-native-picker-select": "^8.1.0",
"react-native-progress": "bluesky-social/react-native-progress",
"react-native-reanimated": "^3.4.2",
"react-native-reanimated": "^3.6.0",
"react-native-root-siblings": "^4.1.1",
"react-native-safe-area-context": "4.6.3",
"react-native-screens": "~3.22.0",
@@ -157,15 +165,18 @@
"sentry-expo": "~7.0.1",
"tippy.js": "^6.3.7",
"tlds": "^1.234.0",
"use-deep-compare": "^1.1.0",
"zeego": "^1.6.2",
"zod": "^3.20.2"
},
"devDependencies": {
"@atproto/dev-env": "^0.2.5",
"@babel/core": "^7.20.0",
"@atproto/dev-env": "^0.2.19",
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@did-plc/server": "^0.0.1",
"@lingui/cli": "^4.5.0",
"@lingui/macro": "^4.5.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@react-native-community/eslint-config": "^3.0.0",
"@testing-library/jest-native": "^5.4.1",
@@ -190,12 +201,14 @@
"@typescript-eslint/parser": "^5.48.2",
"babel-jest": "^29.4.2",
"babel-loader": "^9.1.2",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-module-resolver": "^5.0.0",
"babel-plugin-react-native-web": "^0.18.12",
"detox": "^20.13.0",
"eslint": "^8.19.0",
"eslint-plugin-detox": "^1.0.0",
"eslint-plugin-ft-flow": "^2.0.3",
"eslint-plugin-lingui": "^0.2.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-native-a11y": "^3.3.0",
"html-webpack-plugin": "^5.5.0",
@@ -236,8 +249,11 @@
"json",
"node"
],
"transform": {
"\\.[jt]sx?$": "babel-jest"
},
"transformIgnorePatterns": [
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo|bcp-47-match)"
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|nanoid|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo|bcp-47-match)"
],
"modulePathIgnorePatterns": [
"__tests__/.*/__mocks__",
+44
View File
@@ -0,0 +1,44 @@
diff --git a/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js b/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js
index 48a1409..ef185c9 100644
--- a/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js
+++ b/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js
@@ -70,14 +70,19 @@ function wrapModule(
importDefaultName,
importAllName,
dependencyMapName,
- globalPrefix
+ globalPrefix,
+ moduleFactoryName
) {
const params = buildParameters(
importDefaultName,
importAllName,
dependencyMapName
);
- const factory = functionFromProgram(fileAst.program, params);
+ const factory = functionFromProgram(
+ fileAst.program,
+ params,
+ moduleFactoryName
+ );
const def = t.callExpression(t.identifier(`${globalPrefix}__d`), [factory]);
const ast = t.file(t.program([t.expressionStatement(def)]));
const requireName = renameRequires(ast);
@@ -107,7 +112,16 @@ function wrapJson(source, globalPrefix) {
"});",
].join("\n");
}
-function functionFromProgram(program, parameters) {
+const JS_INVALID_IDENT_RE = /[^a-zA-Z0-9$_]/g;
+function functionFromProgram(program, parameters, moduleFactoryName) {
+ let identifier;
+ if (typeof moduleFactoryName === "string" && moduleFactoryName !== "") {
+ // Keep the name readable so it shows up in profiler traces.
+ // Add an unlikely suffix to avoid collisions with the module code.
+ identifier = t.identifier(
+ `${moduleFactoryName.replace(JS_INVALID_IDENT_RE, "_")}__module_factory__`
+ );
+ }
return t.functionExpression(
undefined,
parameters.map(makeIdentifier),
@@ -0,0 +1,41 @@
diff --git a/node_modules/metro-transform-worker/src/index.js b/node_modules/metro-transform-worker/src/index.js
index 27d4cb3..fd71f47 100644
--- a/node_modules/metro-transform-worker/src/index.js
+++ b/node_modules/metro-transform-worker/src/index.js
@@ -190,6 +190,10 @@ async function transformJS(file, { config, options, projectRoot }) {
let dependencyMapName = "";
let dependencies;
let wrappedAst;
+ const minify =
+ options.minify &&
+ options.unstable_transformProfile !== "hermes-canary" &&
+ options.unstable_transformProfile !== "hermes-stable";
// If the module to transform is a script (meaning that is not part of the
// dependency graph and it code will just be prepended to the bundle modules),
@@ -229,19 +233,20 @@ async function transformJS(file, { config, options, projectRoot }) {
if (config.unstable_disableModuleWrapping === true) {
wrappedAst = ast;
} else {
+ let moduleFactoryName;
+ if (options.dev && !minify) {
+ moduleFactoryName = file.filename;
+ }
({ ast: wrappedAst } = JsFileWrapping.wrapModule(
ast,
importDefault,
importAll,
dependencyMapName,
- config.globalPrefix
+ config.globalPrefix,
+ moduleFactoryName
));
}
}
- const minify =
- options.minify &&
- options.unstable_transformProfile !== "hermes-canary" &&
- options.unstable_transformProfile !== "hermes-stable";
const reserved = [];
if (config.unstable_dependencyMapReservedName != null) {
reserved.push(config.unstable_dependencyMapReservedName);
+14
View File
@@ -0,0 +1,14 @@
const {withAndroidManifest} = require('expo/config-plugins')
module.exports = function withAndroidManifestPlugin(appConfig) {
return withAndroidManifest(appConfig, function (decoratedAppConfig) {
try {
decoratedAppConfig.modResults.manifest.application[0].$[
'android:largeHeap'
] = 'true'
} catch (e) {
console.error(`withAndroidManifestPlugin failed`, e)
}
return decoratedAppConfig
})
}
+90 -31
View File
@@ -5,61 +5,120 @@ 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 {observer} from 'mobx-react-lite'
import {QueryClientProvider} from '@tanstack/react-query'
import {
SafeAreaProvider,
initialWindowMetrics,
} from 'react-native-safe-area-context'
import 'view/icons'
import {withSentry} from 'lib/sentry'
import {init as initPersistedState} from '#/state/persisted'
import {listenSessionDropped} from './state/events'
import {useColorMode} from 'state/shell'
import {ThemeProvider} from 'lib/ThemeContext'
import {s} from 'lib/styles'
import {RootStoreModel, setupState, RootStoreProvider} from './state'
import {Shell} from 'view/shell'
import * as notifications from 'lib/notifications/notifications'
import * as analytics from 'lib/analytics/analytics'
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 {Provider as LightboxStateProvider} from 'state/lightbox'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import I18nProvider from './locale/i18nProvider'
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'
SplashScreen.preventAutoHideAsync()
const App = observer(function AppImpl() {
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
undefined,
)
function InnerApp() {
const colorMode = useColorMode()
const {isInitialLoad, currentAccount} = useSession()
const {resumeSession} = useSessionApi()
// init
useEffect(() => {
setupState().then(store => {
setRootStore(store)
analytics.init(store)
notifications.init(store)
store.onSessionDropped(() => {
Toast.show('Sorry! Your session expired. Please log in again.')
})
notifications.init(queryClient)
listenSessionDropped(() => {
Toast.show('Sorry! Your session expired. Please log in again.')
})
const account = persisted.get('session').currentAccount
resumeSession(account)
}, [resumeSession])
return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<Splash isReady={!isInitialLoad}>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<LoggedOutViewProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={colorMode}>
{/* All components should be within this provider */}
<RootSiblingParent>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</RootSiblingParent>
</ThemeProvider>
</UnreadNotifsProvider>
</LoggedOutViewProvider>
</React.Fragment>
</Splash>
</SafeAreaProvider>
)
}
function App() {
const [isReady, setReady] = useState(false)
React.useEffect(() => {
initPersistedState().then(() => setReady(true))
}, [])
// show nothing prior to init
if (!rootStore) {
if (!isReady) {
return null
}
/*
* NOTE: only nothing here can depend on other data or session state, since
* that is set up in the InnerApp component above.
*/
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={rootStore.shell.colorMode}>
<RootSiblingParent>
<analytics.Provider>
<RootStoreProvider value={rootStore}>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</RootStoreProvider>
</analytics.Provider>
</RootSiblingParent>
</ThemeProvider>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<LightboxStateProvider>
<I18nProvider>
<InnerApp />
</I18nProvider>
</LightboxStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
</QueryClientProvider>
)
})
}
export default withSentry(App)
export default App
+78 -27
View File
@@ -1,54 +1,105 @@
import 'lib/sentry' // must be near top
import React, {useState, useEffect} from 'react'
import {observer} from 'mobx-react-lite'
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 * as analytics from 'lib/analytics/analytics'
import {RootStoreModel, setupState, RootStoreProvider} from './state'
import {init as initPersistedState} from '#/state/persisted'
import {useColorMode} from 'state/shell'
import {Shell} from 'view/shell/index'
import {ToastContainer} from 'view/com/util/Toast.web'
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 LightboxStateProvider} from 'state/lightbox'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences'
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
import I18nProvider from './locale/i18nProvider'
import {
Provider as SessionProvider,
useSession,
useSessionApi,
} from 'state/session'
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted'
const App = observer(function AppImpl() {
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
undefined,
)
function InnerApp() {
const {isInitialLoad, currentAccount} = useSession()
const {resumeSession} = useSessionApi()
const colorMode = useColorMode()
// init
useEffect(() => {
setupState().then(store => {
setRootStore(store)
analytics.init(store)
})
}, [])
const account = persisted.get('session').currentAccount
resumeSession(account)
}, [resumeSession])
// show nothing prior to init
if (!rootStore) {
return null
}
// wait for session to resume
if (isInitialLoad) return null
return (
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={rootStore.shell.colorMode}>
<RootSiblingParent>
<analytics.Provider>
<RootStoreProvider value={rootStore}>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<LoggedOutViewProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={colorMode}>
{/* All components should be within this provider */}
<RootSiblingParent>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
<ToastContainer />
</RootStoreProvider>
</analytics.Provider>
</RootSiblingParent>
</ThemeProvider>
</RootSiblingParent>
<ToastContainer />
</ThemeProvider>
</UnreadNotifsProvider>
</LoggedOutViewProvider>
</React.Fragment>
)
}
function App() {
const [isReady, setReady] = useState(false)
React.useEffect(() => {
initPersistedState().then(() => setReady(true))
}, [])
if (!isReady) {
return null
}
/*
* NOTE: only nothing here can depend on other data or session state, since
* that is set up in the InnerApp component above.
*/
return (
<QueryClientProvider client={queryClient}>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<LightboxStateProvider>
<I18nProvider>
<InnerApp />
</I18nProvider>
</LightboxStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider>
</SessionProvider>
</QueryClientProvider>
)
})
}
export default App
+95 -66
View File
@@ -1,7 +1,5 @@
import * as React from 'react'
import {StyleSheet} from 'react-native'
import * as SplashScreen from 'expo-splash-screen'
import {observer} from 'mobx-react-lite'
import {
NavigationContainer,
createNavigationContainerRef,
@@ -10,7 +8,6 @@ import {
DefaultTheme,
DarkTheme,
} from '@react-navigation/native'
import {createNativeStackNavigator} from '@react-navigation/native-stack'
import {
BottomTabBarProps,
createBottomTabNavigator,
@@ -33,26 +30,33 @@ import {isNative} from 'platform/detection'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {router} from './routes'
import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from './state'
import {getRoutingInstrumentation} from 'lib/sentry'
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 {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 {ModerationMuteListsScreen} from './view/screens/ModerationMuteLists'
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 {CustomFeedScreen} from './view/screens/CustomFeed'
import {CustomFeedLikedByScreen} from './view/screens/CustomFeedLikedBy'
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'
@@ -70,16 +74,18 @@ import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
import {SavedFeeds} from 'view/screens/SavedFeeds'
import {PreferencesHomeFeed} from 'view/screens/PreferencesHomeFeed'
import {PreferencesThreads} from 'view/screens/PreferencesThreads'
import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStackNavigatorWithAuth'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
const HomeTab = createNativeStackNavigator<HomeTabNavigatorParams>()
const SearchTab = createNativeStackNavigator<SearchTabNavigatorParams>()
const FeedsTab = createNativeStackNavigator<FeedsTabNavigatorParams>()
const HomeTab = createNativeStackNavigatorWithAuth<HomeTabNavigatorParams>()
const SearchTab = createNativeStackNavigatorWithAuth<SearchTabNavigatorParams>()
const FeedsTab = createNativeStackNavigatorWithAuth<FeedsTabNavigatorParams>()
const NotificationsTab =
createNativeStackNavigator<NotificationsTabNavigatorParams>()
const MyProfileTab = createNativeStackNavigator<MyProfileTabNavigatorParams>()
const Flat = createNativeStackNavigator<FlatNavigatorParams>()
createNativeStackNavigatorWithAuth<NotificationsTabNavigatorParams>()
const MyProfileTab =
createNativeStackNavigatorWithAuth<MyProfileTabNavigatorParams>()
const Flat = createNativeStackNavigatorWithAuth<FlatNavigatorParams>()
const Tab = createBottomTabNavigator<BottomTabNavigatorParams>()
/**
@@ -96,34 +102,39 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
options={{title: title('Not Found')}}
/>
<Stack.Screen
name="Moderation"
getComponent={() => ModerationScreen}
options={{title: title('Moderation')}}
name="Lists"
component={ListsScreen}
options={{title: title('Lists'), requireAuth: true}}
/>
<Stack.Screen
name="ModerationMuteLists"
getComponent={() => ModerationMuteListsScreen}
options={{title: title('Mute Lists')}}
name="Moderation"
getComponent={() => ModerationScreen}
options={{title: title('Moderation'), requireAuth: true}}
/>
<Stack.Screen
name="ModerationModlists"
getComponent={() => ModerationModlistsScreen}
options={{title: title('Moderation Lists'), requireAuth: true}}
/>
<Stack.Screen
name="ModerationMutedAccounts"
getComponent={() => ModerationMutedAccounts}
options={{title: title('Muted Accounts')}}
options={{title: title('Muted Accounts'), requireAuth: true}}
/>
<Stack.Screen
name="ModerationBlockedAccounts"
getComponent={() => ModerationBlockedAccounts}
options={{title: title('Blocked Accounts')}}
options={{title: title('Blocked Accounts'), requireAuth: true}}
/>
<Stack.Screen
name="Settings"
getComponent={() => SettingsScreen}
options={{title: title('Settings')}}
options={{title: title('Settings'), requireAuth: true}}
/>
<Stack.Screen
name="LanguageSettings"
getComponent={() => LanguageSettingsScreen}
options={{title: title('Language Settings')}}
options={{title: title('Language Settings'), requireAuth: true}}
/>
<Stack.Screen
name="Profile"
@@ -150,7 +161,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
<Stack.Screen
name="ProfileList"
getComponent={() => ProfileListScreen}
options={{title: title('Mute List')}}
options={{title: title('List'), requireAuth: true}}
/>
<Stack.Screen
name="PostThread"
@@ -168,24 +179,24 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
options={({route}) => ({title: title(`Post by @${route.params.name}`)})}
/>
<Stack.Screen
name="CustomFeed"
getComponent={() => CustomFeedScreen}
options={{title: title('Feed')}}
name="ProfileFeed"
getComponent={() => ProfileFeedScreen}
options={{title: title('Feed'), requireAuth: true}}
/>
<Stack.Screen
name="CustomFeedLikedBy"
getComponent={() => CustomFeedLikedByScreen}
name="ProfileFeedLikedBy"
getComponent={() => ProfileFeedLikedByScreen}
options={{title: title('Liked by')}}
/>
<Stack.Screen
name="Debug"
getComponent={() => DebugScreen}
options={{title: title('Debug')}}
options={{title: title('Debug'), requireAuth: true}}
/>
<Stack.Screen
name="Log"
getComponent={() => LogScreen}
options={{title: title('Log')}}
options={{title: title('Log'), requireAuth: true}}
/>
<Stack.Screen
name="Support"
@@ -215,22 +226,22 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
<Stack.Screen
name="AppPasswords"
getComponent={() => AppPasswords}
options={{title: title('App Passwords')}}
options={{title: title('App Passwords'), requireAuth: true}}
/>
<Stack.Screen
name="SavedFeeds"
getComponent={() => SavedFeeds}
options={{title: title('Edit My Feeds')}}
options={{title: title('Edit My Feeds'), requireAuth: true}}
/>
<Stack.Screen
name="PreferencesHomeFeed"
getComponent={() => PreferencesHomeFeed}
options={{title: title('Home Feed Preferences')}}
options={{title: title('Home Feed Preferences'), requireAuth: true}}
/>
<Stack.Screen
name="PreferencesThreads"
getComponent={() => PreferencesThreads}
options={{title: title('Threads Preferences')}}
options={{title: title('Threads Preferences'), requireAuth: true}}
/>
</>
)
@@ -281,7 +292,11 @@ function HomeTabNavigator() {
animationDuration: 250,
contentStyle,
}}>
<HomeTab.Screen name="Home" getComponent={() => HomeScreen} />
<HomeTab.Screen
name="Home"
getComponent={() => HomeScreen}
options={{requireAuth: true}}
/>
{commonScreens(HomeTab)}
</HomeTab.Navigator>
)
@@ -315,7 +330,11 @@ function FeedsTabNavigator() {
animationDuration: 250,
contentStyle,
}}>
<FeedsTab.Screen name="Feeds" getComponent={() => FeedsScreen} />
<FeedsTab.Screen
name="Feeds"
getComponent={() => FeedsScreen}
options={{requireAuth: true}}
/>
{commonScreens(FeedsTab as typeof HomeTab)}
</FeedsTab.Navigator>
)
@@ -335,15 +354,15 @@ function NotificationsTabNavigator() {
<NotificationsTab.Screen
name="Notifications"
getComponent={() => NotificationsScreen}
options={{requireAuth: true}}
/>
{commonScreens(NotificationsTab as typeof HomeTab)}
</NotificationsTab.Navigator>
)
}
const MyProfileTabNavigator = observer(function MyProfileTabNavigatorImpl() {
function MyProfileTabNavigator() {
const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark)
const store = useStores()
return (
<MyProfileTab.Navigator
screenOptions={{
@@ -354,28 +373,27 @@ const MyProfileTabNavigator = observer(function MyProfileTabNavigatorImpl() {
contentStyle,
}}>
<MyProfileTab.Screen
name="MyProfile"
// @ts-ignore // TODO: fix this broken type in ProfileScreen
name="MyProfile"
getComponent={() => ProfileScreen}
initialParams={{
name: store.me.did,
name: 'me',
}}
/>
{commonScreens(MyProfileTab as typeof HomeTab)}
</MyProfileTab.Navigator>
)
})
}
/**
* The FlatNavigator is used by Web to represent the routes
* in a single ("flat") stack.
*/
const FlatNavigator = observer(function FlatNavigatorImpl() {
const FlatNavigator = () => {
const pal = usePalette('default')
const store = useStores()
const unreadCountLabel = store.me.notifications.unreadCountLabel
const numUnread = useUnreadNotifications()
const title = (page: string) => bskyTitle(page, unreadCountLabel)
const title = (page: string) => bskyTitle(page, numUnread)
return (
<Flat.Navigator
screenOptions={{
@@ -388,7 +406,7 @@ const FlatNavigator = observer(function FlatNavigatorImpl() {
<Flat.Screen
name="Home"
getComponent={() => HomeScreen}
options={{title: title('Home')}}
options={{title: title('Home'), requireAuth: true}}
/>
<Flat.Screen
name="Search"
@@ -398,17 +416,17 @@ const FlatNavigator = observer(function FlatNavigatorImpl() {
<Flat.Screen
name="Feeds"
getComponent={() => FeedsScreen}
options={{title: title('Feeds')}}
options={{title: title('Feeds'), requireAuth: true}}
/>
<Flat.Screen
name="Notifications"
getComponent={() => NotificationsScreen}
options={{title: title('Notifications')}}
options={{title: title('Notifications'), requireAuth: true}}
/>
{commonScreens(Flat as typeof HomeTab, unreadCountLabel)}
{commonScreens(Flat as typeof HomeTab, numUnread)}
</Flat.Navigator>
)
})
}
/**
* The RoutesContainer should wrap all components which need access
@@ -460,25 +478,26 @@ const LINKING = {
function RoutesContainer({children}: React.PropsWithChildren<{}>) {
const theme = useColorSchemeStyle(DefaultTheme, DarkTheme)
const {currentAccount} = useSession()
const {openModal} = useModalControls()
function onReady() {
initAnalytics(currentAccount)
if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) {
openModal({name: 'verify-email', showReminder: true})
setEmailConfirmationRequested()
}
}
return (
<NavigationContainer
ref={navigationRef}
linking={LINKING}
theme={theme}
onReady={() => {
SplashScreen.hideAsync()
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`)
logModuleInitTrace()
// Register the navigation container with the Sentry instrumentation (only works on native)
if (isNative) {
const routingInstrumentation = getRoutingInstrumentation()
routingInstrumentation.registerNavigationContainer(navigationRef)
}
logModuleInitTime()
onReady()
}}>
{children}
</NavigationContainer>
@@ -587,7 +606,17 @@ const styles = StyleSheet.create({
},
})
function logModuleInitTrace() {
let didInit = false
function logModuleInitTime() {
if (didInit) {
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`)
if (__DEV__) {
// This log is noisy, so keep false committed
const shouldLog = false
+164
View File
@@ -0,0 +1,164 @@
import React, {useCallback, useEffect} from 'react'
import {View, StyleSheet, Image as RNImage} from 'react-native'
import * as SplashScreen from 'expo-splash-screen'
import {Image} from 'expo-image'
import Animated, {
interpolate,
runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
Easing,
} from 'react-native-reanimated'
import MaskedView from '@react-native-masked-view/masked-view'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import Svg, {Path, SvgProps} from 'react-native-svg'
// @ts-ignore
import splashImagePointer from '../assets/splash.png'
const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri
export const Logo = React.forwardRef(function LogoImpl(props: SvgProps, ref) {
const width = 1000
const height = width * (67 / 64)
return (
<Svg
fill="none"
// @ts-ignore it's fiiiiine
ref={ref}
viewBox="0 0 64 66"
style={{width, height}}>
<Path
fill="#fff"
d="M13.873 3.77C21.21 9.243 29.103 20.342 32 26.3v15.732c0-.335-.13.043-.41.858-1.512 4.414-7.418 21.642-20.923 7.87-7.111-7.252-3.819-14.503 9.125-16.692-7.405 1.252-15.73-.817-18.014-8.93C1.12 22.804 0 8.431 0 6.488 0-3.237 8.579-.18 13.873 3.77ZM50.127 3.77C42.79 9.243 34.897 20.342 32 26.3v15.732c0-.335.13.043.41.858 1.512 4.414 7.418 21.642 20.923 7.87 7.111-7.252 3.819-14.503-9.125-16.692 7.405 1.252 15.73-.817 18.014-8.93C62.88 22.804 64 8.431 64 6.488 64-3.237 55.422-.18 50.127 3.77Z"
/>
</Svg>
)
})
type Props = {
isReady: boolean
}
SplashScreen.preventAutoHideAsync().catch(() => {})
const AnimatedLogo = Animated.createAnimatedComponent(Logo)
export function Splash(props: React.PropsWithChildren<Props>) {
const insets = useSafeAreaInsets()
const intro = useSharedValue(0)
const outroLogo = useSharedValue(0)
const outroApp = useSharedValue(0)
const [isAnimationComplete, setIsAnimationComplete] = React.useState(false)
const [isImageLoaded, setIsImageLoaded] = React.useState(false)
const isReady = props.isReady && isImageLoaded
const logoAnimations = useAnimatedStyle(() => {
return {
transform: [
{
scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'),
},
{
scale: interpolate(
outroLogo.value,
[0, 0.06, 0.08, 1],
[1, 0.8, 0.8, 400],
'clamp',
),
},
],
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
}
})
const appAnimation = useAnimatedStyle(() => {
return {
transform: [
{
scale: interpolate(outroApp.value, [0, 1], [1.1, 1], 'clamp'),
},
],
opacity: interpolate(outroApp.value, [0, 0.9, 1], [0, 1, 1], 'clamp'),
}
})
const onFinish = useCallback(() => setIsAnimationComplete(true), [])
useEffect(() => {
if (isReady) {
// hide on mount
SplashScreen.hideAsync().catch(() => {})
intro.value = withTiming(
1,
{duration: 200, easing: Easing.out(Easing.cubic)},
async () => {
// set these values to check animation at specific point
// outroLogo.value = 0.1
// outroApp.value = 0.1
outroLogo.value = withTiming(
1,
{duration: 1000, easing: Easing.in(Easing.cubic)},
() => {
runOnJS(onFinish)()
},
)
outroApp.value = withTiming(
1,
{duration: 1000, easing: Easing.inOut(Easing.cubic)},
() => {
runOnJS(onFinish)()
},
)
},
)
}
}, [onFinish, intro, outroLogo, outroApp, isReady])
const onLoadEnd = useCallback(() => {
setIsImageLoaded(true)
}, [setIsImageLoaded])
return (
<View style={{flex: 1}}>
{!isAnimationComplete && (
<Image
accessibilityIgnoresInvertColors
onLoadEnd={onLoadEnd}
source={{uri: splashImageUri}}
style={StyleSheet.absoluteFillObject}
/>
)}
<MaskedView
style={[StyleSheet.absoluteFillObject]}
maskElement={
<Animated.View
style={[
StyleSheet.absoluteFillObject,
{
// Transparent background because mask is based off alpha channel.
backgroundColor: 'transparent',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px
},
]}>
<AnimatedLogo style={[logoAnimations]} />
</Animated.View>
}>
{!isAnimationComplete && (
<View
style={[StyleSheet.absoluteFillObject, {backgroundColor: 'white'}]}
/>
)}
<Animated.View style={[{flex: 1}, appAnimation]}>
{props.children}
</Animated.View>
</MaskedView>
</View>
)
}
+9
View File
@@ -0,0 +1,9 @@
export const IS_TEST = process.env.EXPO_PUBLIC_ENV === 'test'
export const IS_DEV = __DEV__
export const IS_PROD = !IS_DEV
export const LOG_DEBUG = process.env.EXPO_PUBLIC_LOG_DEBUG || ''
export const LOG_LEVEL = (process.env.EXPO_PUBLIC_LOG_LEVEL || 'info') as
| 'debug'
| 'info'
| 'warn'
| 'error'
+35
View File
@@ -0,0 +1,35 @@
import React, {createContext, useContext, useMemo} from 'react'
import {ScrollHandlers} from 'react-native-reanimated'
const ScrollContext = createContext<ScrollHandlers<any>>({
onBeginDrag: undefined,
onEndDrag: undefined,
onScroll: undefined,
})
export function useScrollHandlers(): ScrollHandlers<any> {
return useContext(ScrollContext)
}
type ProviderProps = {children: React.ReactNode} & ScrollHandlers<any>
// Note: this completely *overrides* the parent handlers.
// It's up to you to compose them with the parent ones via useScrollHandlers() if needed.
export function ScrollProvider({
children,
onBeginDrag,
onEndDrag,
onScroll,
}: ProviderProps) {
const handlers = useMemo(
() => ({
onBeginDrag,
onEndDrag,
onScroll,
}),
[onBeginDrag, onEndDrag, onScroll],
)
return (
<ScrollContext.Provider value={handlers}>{children}</ScrollContext.Provider>
)
}
+98 -81
View File
@@ -1,131 +1,148 @@
import React from 'react'
import {AppState, AppStateStatus} from 'react-native'
import {
createClient,
AnalyticsProvider,
useAnalytics as useAnalyticsOrig,
ClientMethods,
} from '@segment/analytics-react-native'
import {RootStoreModel, AppInfo} from 'state/models/root-store'
import {useStores} from 'state/models/root-store'
import AsyncStorage from '@react-native-async-storage/async-storage'
import {createClient, SegmentClient} from '@segment/analytics-react-native'
import {sha256} from 'js-sha256'
import {ScreenEvent, TrackEvent} from './types'
import {Native} from 'sentry-expo'
const segmentClient = createClient({
writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
trackAppLifecycleEvents: false,
proxy: 'https://api.events.bsky.app/v1',
})
import {useSession, SessionAccount} from '#/state/session'
import {TrackEvent, AnalyticsMethods} from './types'
import {logger} from '#/logger'
export const track = segmentClient?.track?.bind?.(segmentClient) as TrackEvent
type AppInfo = {
build?: string | undefined
name?: string | undefined
namespace?: string | undefined
version?: string | undefined
}
export function useAnalytics() {
const store = useStores()
const methods: ClientMethods = useAnalyticsOrig()
// Delay creating until first actual use.
let segmentClient: SegmentClient | null = null
function getClient(): SegmentClient {
if (!segmentClient) {
segmentClient = createClient({
writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
trackAppLifecycleEvents: false,
proxy: 'https://api.events.bsky.app/v1',
})
}
return segmentClient
}
export const track: TrackEvent = async (...args) => {
await getClient().track(...args)
}
export function useAnalytics(): AnalyticsMethods {
const {hasSession} = useSession()
return React.useMemo(() => {
if (store.session.hasSession) {
if (hasSession) {
return {
screen: methods.screen as ScreenEvent, // ScreenEvents defines all the possible screen names
track: methods.track as TrackEvent, // TrackEvents defines all the possible track events and their properties
identify: methods.identify,
flush: methods.flush,
group: methods.group,
alias: methods.alias,
reset: methods.reset,
async screen(...args) {
await getClient().screen(...args)
},
async track(...args) {
await getClient().track(...args)
},
}
}
// dont send analytics pings for anonymous users
return {
screen: () => Promise<void>,
track: () => Promise<void>,
identify: () => Promise<void>,
flush: () => Promise<void>,
group: () => Promise<void>,
alias: () => Promise<void>,
reset: () => Promise<void>,
screen: async () => {},
track: async () => {},
}
}, [store, methods])
}, [hasSession])
}
export function init(store: RootStoreModel) {
store.onSessionLoaded(() => {
const sess = store.session.currentSession
if (sess) {
if (sess.did) {
const did_hashed = sha256(sess.did)
segmentClient.identify(did_hashed, {did_hashed})
store.log.debug('Ping w/hash')
} else {
store.log.debug('Ping w/o hash')
segmentClient.identify()
}
}
})
export function init(account: SessionAccount | undefined) {
setupListenersOnce()
if (account) {
const client = getClient()
if (account.did) {
const did_hashed = sha256(account.did)
client.identify(did_hashed, {did_hashed})
Native.setUser({id: did_hashed})
logger.debug('Ping w/hash')
} else {
logger.debug('Ping w/o hash')
client.identify()
}
}
}
let didSetupListeners = false
function setupListenersOnce() {
if (didSetupListeners) {
return
}
didSetupListeners = true
// NOTE
// this is a copy of segment's own lifecycle event tracking
// we handle it manually to ensure that it never fires while the app is backgrounded
// -prf
segmentClient.isReady.onChange(() => {
const client = getClient()
client.isReady.onChange(async () => {
if (AppState.currentState !== 'active') {
store.log.debug('Prevented a metrics ping while the app was backgrounded')
logger.debug('Prevented a metrics ping while the app was backgrounded')
return
}
const context = segmentClient.context.get()
const context = client.context.get()
if (typeof context?.app === 'undefined') {
store.log.debug('Aborted metrics ping due to unavailable context')
logger.debug('Aborted metrics ping due to unavailable context')
return
}
const oldAppInfo = store.appInfo
const oldAppInfo = await readAppInfo()
const newAppInfo = context.app as AppInfo
store.setAppInfo(newAppInfo)
store.log.debug('Recording app info', {new: newAppInfo, old: oldAppInfo})
writeAppInfo(newAppInfo)
logger.debug('Recording app info', {new: newAppInfo, old: oldAppInfo})
if (typeof oldAppInfo === 'undefined') {
if (store.session.hasSession) {
segmentClient.track('Application Installed', {
version: newAppInfo.version,
build: newAppInfo.build,
})
}
} else if (newAppInfo.version !== oldAppInfo.version) {
if (store.session.hasSession) {
segmentClient.track('Application Updated', {
version: newAppInfo.version,
build: newAppInfo.build,
previous_version: oldAppInfo.version,
previous_build: oldAppInfo.build,
})
}
}
if (store.session.hasSession) {
segmentClient.track('Application Opened', {
from_background: false,
client.track('Application Installed', {
version: newAppInfo.version,
build: newAppInfo.build,
})
} else if (newAppInfo.version !== oldAppInfo.version) {
client.track('Application Updated', {
version: newAppInfo.version,
build: newAppInfo.build,
previous_version: oldAppInfo.version,
previous_build: oldAppInfo.build,
})
}
client.track('Application Opened', {
from_background: false,
version: newAppInfo.version,
build: newAppInfo.build,
})
})
let lastState: AppStateStatus = AppState.currentState
AppState.addEventListener('change', (state: AppStateStatus) => {
if (state === 'active' && lastState !== 'active') {
const context = segmentClient.context.get()
segmentClient.track('Application Opened', {
const context = client.context.get()
client.track('Application Opened', {
from_background: true,
version: context?.app?.version,
build: context?.app?.build,
})
} else if (state !== 'active' && lastState === 'active') {
segmentClient.track('Application Backgrounded')
client.track('Application Backgrounded')
}
lastState = state
})
}
export function Provider({children}: React.PropsWithChildren<{}>) {
return (
<AnalyticsProvider client={segmentClient}>{children}</AnalyticsProvider>
)
async function writeAppInfo(value: AppInfo) {
await AsyncStorage.setItem('BSKY_APP_INFO', JSON.stringify(value))
}
async function readAppInfo(): Promise<AppInfo | undefined> {
const rawData = await AsyncStorage.getItem('BSKY_APP_INFO')
const obj = rawData ? JSON.parse(rawData) : undefined
if (!obj || typeof obj !== 'object') {
return undefined
}
return obj
}
+56 -51
View File
@@ -1,65 +1,70 @@
import React from 'react'
import {
createClient,
AnalyticsProvider,
useAnalytics as useAnalyticsOrig,
} from '@segment/analytics-react'
import {RootStoreModel} from 'state/models/root-store'
import {useStores} from 'state/models/root-store'
import {createClient} from '@segment/analytics-react'
import {sha256} from 'js-sha256'
import {Browser} from 'sentry-expo'
const segmentClient = createClient(
{
writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
},
{
integrations: {
'Segment.io': {
apiHost: 'api.events.bsky.app/v1',
import {TrackEvent, AnalyticsMethods} from './types'
import {useSession, SessionAccount} from '#/state/session'
import {logger} from '#/logger'
type SegmentClient = ReturnType<typeof createClient>
// Delay creating until first actual use.
let segmentClient: SegmentClient | null = null
function getClient(): SegmentClient {
if (!segmentClient) {
segmentClient = createClient(
{
writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
},
},
},
)
export const track = segmentClient?.track?.bind?.(segmentClient)
{
integrations: {
'Segment.io': {
apiHost: 'api.events.bsky.app/v1',
},
},
},
)
}
return segmentClient
}
export function useAnalytics() {
const store = useStores()
const methods = useAnalyticsOrig()
export const track: TrackEvent = async (...args) => {
await getClient().track(...args)
}
export function useAnalytics(): AnalyticsMethods {
const {hasSession} = useSession()
return React.useMemo(() => {
if (store.session.hasSession) {
return methods
if (hasSession) {
return {
async screen(...args) {
await getClient().screen(...args)
},
async track(...args) {
await getClient().track(...args)
},
}
}
// dont send analytics pings for anonymous users
return {
screen: () => {},
track: () => {},
identify: () => {},
flush: () => {},
group: () => {},
alias: () => {},
reset: () => {},
screen: async () => {},
track: async () => {},
}
}, [store, methods])
}, [hasSession])
}
export function init(store: RootStoreModel) {
store.onSessionLoaded(() => {
const sess = store.session.currentSession
if (sess) {
if (sess.did) {
const did_hashed = sha256(sess.did)
segmentClient.identify(did_hashed, {did_hashed})
store.log.debug('Ping w/hash')
} else {
store.log.debug('Ping w/o hash')
segmentClient.identify()
}
export function init(account: SessionAccount | undefined) {
if (account) {
const client = getClient()
if (account.did) {
const did_hashed = sha256(account.did)
client.identify(did_hashed, {did_hashed})
Browser.setUser({id: did_hashed})
logger.debug('Ping w/hash')
} else {
logger.debug('Ping w/o hash')
client.identify()
}
})
}
export function Provider({children}: React.PropsWithChildren<{}>) {
return (
<AnalyticsProvider client={segmentClient}>{children}</AnalyticsProvider>
)
}
}
+17 -11
View File
@@ -7,6 +7,7 @@ export type ScreenEvent = (
name: keyof ScreenPropertiesMap,
properties?: ScreenPropertiesMap[keyof ScreenPropertiesMap],
) => Promise<void>
interface TrackPropertiesMap {
// LOGIN / SIGN UP events
'Sign In': {resumedSession: boolean} // CAN BE SERVER
@@ -20,6 +21,7 @@ interface TrackPropertiesMap {
'Composer:PastedPhotos': {}
'Composer:CameraOpened': {}
'Composer:GalleryOpened': {}
'Composer:ThreadgateOpened': {}
'HomeScreen:PressCompose': {}
'ProfileScreen:PressCompose': {}
// EDIT PROFILE events
@@ -41,12 +43,6 @@ interface TrackPropertiesMap {
'Post:ThreadMute': {} // CAN BE SERVER
'Post:ThreadUnmute': {} // CAN BE SERVER
'Post:Reply': {} // CAN BE SERVER
// FEED ITEM events
'FeedItem:PostReply': {} // CAN BE SERVER
'FeedItem:PostRepost': {} // CAN BE SERVER
'FeedItem:PostLike': {} // CAN BE SERVER
'FeedItem:PostDelete': {} // CAN BE SERVER
'FeedItem:ThreadMute': {} // CAN BE SERVER
// PROFILE events
'Profile:Follow': {
username: string
@@ -79,7 +75,6 @@ interface TrackPropertiesMap {
'Settings:AddAccountButtonClicked': {}
'Settings:ChangeHandleButtonClicked': {}
'Settings:InvitecodesButtonClicked': {}
'Settings:ContentfilteringButtonClicked': {}
'Settings:SignOutButtonClicked': {}
'Settings:ContentlanguagesButtonClicked': {}
// MENU events
@@ -97,10 +92,15 @@ interface TrackPropertiesMap {
// LISTS events
'Lists:onRefresh': {}
'Lists:onEndReached': {}
'CreateMuteList:AvatarSelected': {}
'CreateMuteList:Save': {} // CAN BE SERVER
'Lists:Subscribe': {} // CAN BE SERVER
'Lists:Unsubscribe': {} // CAN BE SERVER
'CreateList:AvatarSelected': {}
'CreateList:SaveCurateList': {} // CAN BE SERVER
'CreateList:SaveModList': {} // CAN BE SERVER
'Lists:Mute': {} // CAN BE SERVER
'Lists:Unmute': {} // CAN BE SERVER
'Lists:Block': {} // CAN BE SERVER
'Lists:Unblock': {} // CAN BE SERVER
'Lists:Delete': {} // CAN BE SERVER
'Lists:Share': {} // CAN BE SERVER
// CUSTOM FEED events
'CustomFeed:Save': {}
'CustomFeed:Unsave': {}
@@ -131,6 +131,7 @@ interface TrackPropertiesMap {
'Onboarding:Skipped': {}
'Onboarding:Reset': {}
'Onboarding:SuggestedFollowFollowed': {}
'Onboarding:CustomFeedAdded': {}
}
interface ScreenPropertiesMap {
@@ -151,3 +152,8 @@ interface ScreenPropertiesMap {
MutedAccounts: {}
SavedFeeds: {}
}
export type AnalyticsMethods = {
screen: ScreenEvent
track: TrackEvent
}
+42 -14
View File
@@ -4,7 +4,7 @@ import {
AppBskyEmbedRecordWithMedia,
AppBskyEmbedRecord,
} from '@atproto/api'
import {FeedSourceInfo} from './feed/types'
import {ReasonFeedSource} from './feed/types'
import {isPostInLanguage} from '../../locale/helpers'
type FeedViewPost = AppBskyFeedDefs.FeedViewPost
@@ -14,13 +14,13 @@ export type FeedTunerFn = (
) => FeedViewPostsSlice[]
export class FeedViewPostsSlice {
_reactKey: string
isFlattenedReply = false
constructor(public items: FeedViewPost[] = []) {}
get _reactKey() {
return `slice-${this.items[0].post.uri}-${
this.items[0].reason?.indexedAt || this.items[0].post.indexedAt
constructor(public items: FeedViewPost[]) {
const item = items[0]
this._reactKey = `slice-${item.post.uri}-${
item.reason?.indexedAt || item.post.indexedAt
}`
}
@@ -65,9 +65,9 @@ export class FeedViewPostsSlice {
)
}
get source(): FeedSourceInfo | undefined {
get source(): ReasonFeedSource | undefined {
return this.items.find(item => '__source' in item && !!item.__source)
?.__source as FeedSourceInfo
?.__source as ReasonFeedSource
}
containsUri(uri: string) {
@@ -116,18 +116,33 @@ export class FeedViewPostsSlice {
}
}
export class FeedTuner {
seenUris: Set<string> = new Set()
constructor() {}
export class NoopFeedTuner {
private keyCounter = 0
reset() {
this.keyCounter = 0
}
tune(
feed: FeedViewPost[],
_opts?: {dryRun: boolean; maintainOrder: boolean},
): FeedViewPostsSlice[] {
return feed.map(item => new FeedViewPostsSlice([item]))
}
}
export class FeedTuner {
private keyCounter = 0
seenUris: Set<string> = new Set()
constructor(public tunerFns: FeedTunerFn[]) {}
reset() {
this.keyCounter = 0
this.seenUris.clear()
}
tune(
feed: FeedViewPost[],
tunerFns: FeedTunerFn[] = [],
{dryRun, maintainOrder}: {dryRun: boolean; maintainOrder: boolean} = {
dryRun: false,
maintainOrder: false,
@@ -135,6 +150,19 @@ export class FeedTuner {
): FeedViewPostsSlice[] {
let slices: FeedViewPostsSlice[] = []
// remove posts that are replies, but which don't have the parent
// hydrated. this means the parent was either deleted or blocked
feed = feed.filter(item => {
if (
AppBskyFeedPost.isRecord(item.post.record) &&
item.post.record.reply &&
!item.reply
) {
return false
}
return true
})
if (maintainOrder) {
slices = feed.map(item => new FeedViewPostsSlice([item]))
} else {
@@ -157,7 +185,7 @@ export class FeedTuner {
}
// run the custom tuners
for (const tunerFn of tunerFns) {
for (const tunerFn of this.tunerFns) {
slices = tunerFn(this, slices.slice())
}
+15 -19
View File
@@ -2,37 +2,33 @@ import {
AppBskyFeedDefs,
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
} from '@atproto/api'
import {RootStoreModel} from 'state/index'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class AuthorFeedAPI implements FeedAPI {
cursor: string | undefined
constructor(
public rootStore: RootStoreModel,
public params: GetAuthorFeed.QueryParams,
) {}
reset() {
this.cursor = undefined
}
constructor(public params: GetAuthorFeed.QueryParams) {}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.rootStore.agent.getAuthorFeed({
const res = await getAgent().getAuthorFeed({
...this.params,
limit: 1,
})
return res.data.feed[0]
}
async fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> {
const res = await this.rootStore.agent.getAuthorFeed({
async fetch({
cursor,
limit,
}: {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await getAgent().getAuthorFeed({
...this.params,
cursor: this.cursor,
cursor,
limit,
})
if (res.success) {
this.cursor = res.data.cursor
return {
cursor: res.data.cursor,
feed: this._filter(res.data.feed),
@@ -44,7 +40,7 @@ export class AuthorFeedAPI implements FeedAPI {
}
_filter(feed: AppBskyFeedDefs.FeedViewPost[]) {
if (this.params.filter === 'posts_no_replies') {
if (this.params.filter === 'posts_and_author_threads') {
return feed.filter(post => {
const isReply = post.reply
const isRepost = AppBskyFeedDefs.isReasonRepost(post.reason)
@@ -64,13 +60,13 @@ function isAuthorReplyChain(
posts: AppBskyFeedDefs.FeedViewPost[],
): boolean {
// current post is by a different user (shouldn't happen)
if (post.post.author.handle !== actor) return false
if (post.post.author.did !== actor) return false
const replyParent = post.reply?.parent
if (AppBskyFeedDefs.isPostView(replyParent)) {
// reply parent is by a different user
if (replyParent.author.handle !== actor) return false
if (replyParent.author.did !== actor) return false
// A top-level post that matches the parent of the current post.
const parentPost = posts.find(p => p.post.uri === replyParent.uri)
+13 -17
View File
@@ -2,37 +2,33 @@ import {
AppBskyFeedDefs,
AppBskyFeedGetFeed as GetCustomFeed,
} from '@atproto/api'
import {RootStoreModel} from 'state/index'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class CustomFeedAPI implements FeedAPI {
cursor: string | undefined
constructor(
public rootStore: RootStoreModel,
public params: GetCustomFeed.QueryParams,
) {}
reset() {
this.cursor = undefined
}
constructor(public params: GetCustomFeed.QueryParams) {}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.rootStore.agent.app.bsky.feed.getFeed({
const res = await getAgent().app.bsky.feed.getFeed({
...this.params,
limit: 1,
})
return res.data.feed[0]
}
async fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> {
const res = await this.rootStore.agent.app.bsky.feed.getFeed({
async fetch({
cursor,
limit,
}: {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await getAgent().app.bsky.feed.getFeed({
...this.params,
cursor: this.cursor,
cursor,
limit,
})
if (res.success) {
this.cursor = res.data.cursor
// NOTE
// some custom feeds fail to enforce the pagination limit
// so we manually truncate here
@@ -41,7 +37,7 @@ export class CustomFeedAPI implements FeedAPI {
res.data.feed = res.data.feed.slice(0, limit)
}
return {
cursor: res.data.cursor,
cursor: res.data.feed.length ? res.data.cursor : undefined,
feed: res.data.feed,
}
}
+12 -13
View File
@@ -1,30 +1,29 @@
import {AppBskyFeedDefs} from '@atproto/api'
import {RootStoreModel} from 'state/index'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class FollowingFeedAPI implements FeedAPI {
cursor: string | undefined
constructor(public rootStore: RootStoreModel) {}
reset() {
this.cursor = undefined
}
constructor() {}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.rootStore.agent.getTimeline({
const res = await getAgent().getTimeline({
limit: 1,
})
return res.data.feed[0]
}
async fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> {
const res = await this.rootStore.agent.getTimeline({
cursor: this.cursor,
async fetch({
cursor,
limit,
}: {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await getAgent().getTimeline({
cursor,
limit,
})
if (res.success) {
this.cursor = res.data.cursor
return {
cursor: res.data.cursor,
feed: res.data.feed,
+12 -16
View File
@@ -2,37 +2,33 @@ import {
AppBskyFeedDefs,
AppBskyFeedGetActorLikes as GetActorLikes,
} from '@atproto/api'
import {RootStoreModel} from 'state/index'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class LikesFeedAPI implements FeedAPI {
cursor: string | undefined
constructor(
public rootStore: RootStoreModel,
public params: GetActorLikes.QueryParams,
) {}
reset() {
this.cursor = undefined
}
constructor(public params: GetActorLikes.QueryParams) {}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.rootStore.agent.getActorLikes({
const res = await getAgent().getActorLikes({
...this.params,
limit: 1,
})
return res.data.feed[0]
}
async fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> {
const res = await this.rootStore.agent.getActorLikes({
async fetch({
cursor,
limit,
}: {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await getAgent().getActorLikes({
...this.params,
cursor: this.cursor,
cursor,
limit,
})
if (res.success) {
this.cursor = res.data.cursor
return {
cursor: res.data.cursor,
feed: res.data.feed,
+41
View File
@@ -0,0 +1,41 @@
import {
AppBskyFeedDefs,
AppBskyFeedGetListFeed as GetListFeed,
} from '@atproto/api'
import {FeedAPI, FeedAPIResponse} from './types'
import {getAgent} from '#/state/session'
export class ListFeedAPI implements FeedAPI {
constructor(public params: GetListFeed.QueryParams) {}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await getAgent().app.bsky.feed.getListFeed({
...this.params,
limit: 1,
})
return res.data.feed[0]
}
async fetch({
cursor,
limit,
}: {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
const res = await getAgent().app.bsky.feed.getListFeed({
...this.params,
cursor,
limit,
})
if (res.success) {
return {
cursor: res.data.cursor,
feed: res.data.feed,
}
}
return {
feed: [],
}
}
}
+78 -73
View File
@@ -1,11 +1,13 @@
import {AppBskyFeedDefs, AppBskyFeedGetTimeline} from '@atproto/api'
import shuffle from 'lodash.shuffle'
import {RootStoreModel} from 'state/index'
import {timeout} from 'lib/async/timeout'
import {bundleAsync} from 'lib/async/bundle'
import {feedUriToHref} from 'lib/strings/url-helpers'
import {FeedTuner} from '../feed-manip'
import {FeedAPI, FeedAPIResponse, FeedSourceInfo} from './types'
import {FeedAPI, FeedAPIResponse, ReasonFeedSource} from './types'
import {FeedParams} from '#/state/queries/post-feed'
import {FeedTunerFn} from '../feed-manip'
import {getAgent} from '#/state/session'
const REQUEST_WAIT_MS = 500 // 500ms
const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours
@@ -17,34 +19,50 @@ export class MergeFeedAPI implements FeedAPI {
itemCursor = 0
sampleCursor = 0
constructor(public rootStore: RootStoreModel) {
this.following = new MergeFeedSource_Following(this.rootStore)
constructor(public params: FeedParams, public feedTuners: FeedTunerFn[]) {
this.following = new MergeFeedSource_Following(this.feedTuners)
}
reset() {
this.following = new MergeFeedSource_Following(this.rootStore)
this.following = new MergeFeedSource_Following(this.feedTuners)
this.customFeeds = [] // just empty the array, they will be captured in _fetchNext()
this.feedCursor = 0
this.itemCursor = 0
this.sampleCursor = 0
if (this.params.mergeFeedSources) {
this.customFeeds = shuffle(
this.params.mergeFeedSources.map(
feedUri => new MergeFeedSource_Custom(feedUri, this.feedTuners),
),
)
} else {
this.customFeeds = []
}
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.rootStore.agent.getTimeline({
const res = await getAgent().getTimeline({
limit: 1,
})
return res.data.feed[0]
}
async fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> {
// we capture here to ensure the data has loaded
this._captureFeedsIfNeeded()
async fetch({
cursor,
limit,
}: {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse> {
if (!cursor) {
this.reset()
}
const promises = []
// always keep following topped up
if (this.following.numReady < limit) {
promises.push(this.following.fetchNext(60))
await this.following.fetchNext(60)
}
// pick the next feeds to sample from
@@ -55,9 +73,13 @@ export class MergeFeedAPI implements FeedAPI {
}
// top up the feeds
for (const feed of feeds) {
if (feed.numReady < 5) {
promises.push(feed.fetchNext(10))
const outOfFollows =
!this.following.hasMore && this.following.numReady < limit
if (this.params.mergeFeedEnabled || outOfFollows) {
for (const feed of feeds) {
if (feed.numReady < 5) {
promises.push(feed.fetchNext(10))
}
}
}
@@ -76,7 +98,7 @@ export class MergeFeedAPI implements FeedAPI {
}
return {
cursor: posts.length ? 'fake' : undefined,
cursor: posts.length ? String(this.itemCursor) : undefined,
feed: posts,
}
}
@@ -90,7 +112,10 @@ export class MergeFeedAPI implements FeedAPI {
// this condition establishes the frequency that custom feeds are woven into follows
const shouldSample =
i >= 15 && candidateFeeds.length >= 2 && (i % 4 === 0 || i % 5 === 0)
this.params.mergeFeedEnabled &&
i >= 15 &&
candidateFeeds.length >= 2 &&
(i % 4 === 0 || i % 5 === 0)
if (!canSample && !hasFollows) {
// no data available
@@ -107,33 +132,15 @@ export class MergeFeedAPI implements FeedAPI {
// provide follow
return this.following.take(1)
}
_captureFeedsIfNeeded() {
if (!this.rootStore.preferences.homeFeed.lab_mergeFeedEnabled) {
return
}
if (this.customFeeds.length === 0) {
this.customFeeds = shuffle(
this.rootStore.me.savedFeeds.all.map(
feed =>
new MergeFeedSource_Custom(
this.rootStore,
feed.uri,
feed.displayName,
),
),
)
}
}
}
class MergeFeedSource {
sourceInfo: FeedSourceInfo | undefined
sourceInfo: ReasonFeedSource | undefined
cursor: string | undefined = undefined
queue: AppBskyFeedDefs.FeedViewPost[] = []
hasMore = true
constructor(public rootStore: RootStoreModel) {}
constructor(public feedTuners: FeedTunerFn[]) {}
get numReady() {
return this.queue.length
@@ -180,7 +187,7 @@ class MergeFeedSource {
}
class MergeFeedSource_Following extends MergeFeedSource {
tuner = new FeedTuner()
tuner = new FeedTuner(this.feedTuners)
reset() {
super.reset()
@@ -195,16 +202,12 @@ class MergeFeedSource_Following extends MergeFeedSource {
cursor: string | undefined,
limit: number,
): Promise<AppBskyFeedGetTimeline.Response> {
const res = await this.rootStore.agent.getTimeline({cursor, limit})
const res = await getAgent().getTimeline({cursor, limit})
// run the tuner pre-emptively to ensure better mixing
const slices = this.tuner.tune(
res.data.feed,
this.rootStore.preferences.getFeedTuners('home'),
{
dryRun: false,
maintainOrder: true,
},
)
const slices = this.tuner.tune(res.data.feed, {
dryRun: false,
maintainOrder: true,
})
res.data.feed = slices.map(slice => slice.rootItem)
return res
}
@@ -213,15 +216,12 @@ class MergeFeedSource_Following extends MergeFeedSource {
class MergeFeedSource_Custom extends MergeFeedSource {
minDate: Date
constructor(
public rootStore: RootStoreModel,
public feedUri: string,
public feedDisplayName: string,
) {
super(rootStore)
constructor(public feedUri: string, public feedTuners: FeedTunerFn[]) {
super(feedTuners)
this.sourceInfo = {
displayName: feedDisplayName,
uri: feedUriToHref(feedUri),
$type: 'reasonFeedSource',
uri: feedUri,
href: feedUriToHref(feedUri),
}
this.minDate = new Date(Date.now() - POST_AGE_CUTOFF)
}
@@ -230,26 +230,31 @@ class MergeFeedSource_Custom extends MergeFeedSource {
cursor: string | undefined,
limit: number,
): Promise<AppBskyFeedGetTimeline.Response> {
const res = await this.rootStore.agent.app.bsky.feed.getFeed({
cursor,
limit,
feed: this.feedUri,
})
// NOTE
// some custom feeds fail to enforce the pagination limit
// so we manually truncate here
// -prf
if (limit && res.data.feed.length > limit) {
res.data.feed = res.data.feed.slice(0, limit)
try {
const res = await getAgent().app.bsky.feed.getFeed({
cursor,
limit,
feed: this.feedUri,
})
// NOTE
// some custom feeds fail to enforce the pagination limit
// so we manually truncate here
// -prf
if (limit && res.data.feed.length > limit) {
res.data.feed = res.data.feed.slice(0, limit)
}
// filter out older posts
res.data.feed = res.data.feed.filter(
post => new Date(post.post.indexedAt) > this.minDate,
)
// attach source info
for (const post of res.data.feed) {
post.__source = this.sourceInfo
}
return res
} catch {
// dont bubble custom-feed errors
return {success: false, headers: {}, data: {feed: []}}
}
// filter out older posts
res.data.feed = res.data.feed.filter(
post => new Date(post.post.indexedAt) > this.minDate,
)
// attach source info
for (const post of res.data.feed) {
post.__source = this.sourceInfo
}
return res
}
}
+19 -4
View File
@@ -6,12 +6,27 @@ export interface FeedAPIResponse {
}
export interface FeedAPI {
reset(): void
peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost>
fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse>
fetch({
cursor,
limit,
}: {
cursor: string | undefined
limit: number
}): Promise<FeedAPIResponse>
}
export interface FeedSourceInfo {
export interface ReasonFeedSource {
$type: 'reasonFeedSource'
uri: string
displayName: string
href: string
}
export function isReasonFeedSource(v: unknown): v is ReasonFeedSource {
return (
!!v &&
typeof v === 'object' &&
'$type' in v &&
v.$type === 'reasonFeedSource'
)
}
+70 -42
View File
@@ -3,18 +3,21 @@ import {
AppBskyEmbedExternal,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedThreadgate,
AppBskyRichtextFacet,
BskyAgent,
ComAtprotoLabelDefs,
ComAtprotoRepoUploadBlob,
RichText,
} from '@atproto/api'
import {AtUri} from '@atproto/api'
import {RootStoreModel} from 'state/models/root-store'
import {isNetworkError} from 'lib/strings/errors'
import {LinkMeta} from '../link-meta/link-meta'
import {isWeb} from 'platform/detection'
import {ImageModel} from 'state/models/media/image'
import {shortenLinks} from 'lib/strings/rich-text-manip'
import {logger} from '#/logger'
import {ThreadgateSetting} from '#/state/queries/threadgate'
export interface ExternalEmbedDraft {
uri: string
@@ -24,46 +27,19 @@ export interface ExternalEmbedDraft {
localThumb?: ImageModel
}
export async function resolveName(store: RootStoreModel, didOrHandle: string) {
if (!didOrHandle) {
throw new Error('Invalid handle: ""')
}
if (didOrHandle.startsWith('did:')) {
return didOrHandle
}
// we run the resolution always to ensure freshness
const promise = store.agent
.resolveHandle({
handle: didOrHandle,
})
.then(res => {
store.handleResolutions.cache.set(didOrHandle, res.data.did)
return res.data.did
})
// but we can return immediately if it's cached
const cached = store.handleResolutions.cache.get(didOrHandle)
if (cached) {
return cached
}
return promise
}
export async function uploadBlob(
store: RootStoreModel,
agent: BskyAgent,
blob: string,
encoding: string,
): Promise<ComAtprotoRepoUploadBlob.Response> {
if (isWeb) {
// `blob` should be a data uri
return store.agent.uploadBlob(convertDataURIToUint8Array(blob), {
return agent.uploadBlob(convertDataURIToUint8Array(blob), {
encoding,
})
} else {
// `blob` should be a path to a file in the local FS
return store.agent.uploadBlob(
return agent.uploadBlob(
blob, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts
{encoding},
)
@@ -80,12 +56,12 @@ interface PostOpts {
extLink?: ExternalEmbedDraft
images?: ImageModel[]
labels?: string[]
knownHandles?: Set<string>
threadgate?: ThreadgateSetting[]
onStateChange?: (state: string) => void
langs?: string[]
}
export async function post(store: RootStoreModel, opts: PostOpts) {
export async function post(agent: BskyAgent, opts: PostOpts) {
let embed:
| AppBskyEmbedImages.Main
| AppBskyEmbedExternal.Main
@@ -101,7 +77,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
)
opts.onStateChange?.('Processing...')
await rt.detectFacets(store.agent)
await rt.detectFacets(agent)
rt = shortenLinks(rt)
// filter out any mention facets that didn't map to a user
@@ -128,13 +104,19 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
// add image embed if present
if (opts.images?.length) {
logger.info(`Uploading images`, {
count: opts.images.length,
})
const images: AppBskyEmbedImages.Image[] = []
for (const image of opts.images) {
opts.onStateChange?.(`Uploading image #${images.length + 1}...`)
logger.info(`Compressing image`)
await image.compress()
const path = image.compressed?.path ?? image.path
const {width, height} = image.compressed || image
const res = await uploadBlob(store, path, 'image/jpeg')
logger.info(`Uploading image`)
const res = await uploadBlob(agent, path, 'image/jpeg')
images.push({
image: res.data.blob,
alt: image.altText ?? '',
@@ -178,14 +160,13 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
) {
encoding = 'image/jpeg'
} else {
store.log.warn(
'Unexpected image format for thumbnail, skipping',
opts.extLink.localThumb.path,
)
logger.warn('Unexpected image format for thumbnail, skipping', {
thumbnail: opts.extLink.localThumb.path,
})
}
if (encoding) {
const thumbUploadRes = await uploadBlob(
store,
agent,
opts.extLink.localThumb.path,
encoding,
)
@@ -224,7 +205,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
// add replyTo if post is a reply to another post
if (opts.replyTo) {
const replyToUrip = new AtUri(opts.replyTo)
const parentPost = await store.agent.getPost({
const parentPost = await agent.getPost({
repo: replyToUrip.host,
rkey: replyToUrip.rkey,
})
@@ -255,9 +236,10 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
langs = opts.langs.slice(0, 3)
}
let res
try {
opts.onStateChange?.('Posting...')
return await store.agent.post({
res = await agent.post({
text: rt.text,
facets: rt.facets,
reply,
@@ -275,6 +257,52 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
throw e
}
}
try {
// TODO: this needs to be batch-created with the post!
if (opts.threadgate?.length) {
await createThreadgate(agent, res.uri, opts.threadgate)
}
} catch (e: any) {
console.error(`Failed to create threadgate: ${e.toString()}`)
throw new Error(
'Post reply-controls failed to be set. Your post was created but anyone can reply to it.',
)
}
return res
}
async function createThreadgate(
agent: BskyAgent,
postUri: string,
threadgate: ThreadgateSetting[],
) {
let allow: (
| AppBskyFeedThreadgate.MentionRule
| AppBskyFeedThreadgate.FollowingRule
| AppBskyFeedThreadgate.ListRule
)[] = []
if (!threadgate.find(v => v.type === 'nobody')) {
for (const rule of threadgate) {
if (rule.type === 'mention') {
allow.push({$type: 'app.bsky.feed.threadgate#mentionRule'})
} else if (rule.type === 'following') {
allow.push({$type: 'app.bsky.feed.threadgate#followingRule'})
} else if (rule.type === 'list') {
allow.push({
$type: 'app.bsky.feed.threadgate#listRule',
list: rule.list,
})
}
}
}
const postUrip = new AtUri(postUri)
await agent.api.app.bsky.feed.threadgate.create(
{repo: agent.session!.did, rkey: postUrip.rkey},
{post: postUri, createdAt: new Date().toISOString(), allow},
)
}
// helpers
+2 -3
View File
@@ -1,5 +1,4 @@
import {ImageRequireSource} from 'react-native'
export const DEF_AVATAR: ImageRequireSource = require('../../assets/default-avatar.jpg')
export const TABS_EXPLAINER: ImageRequireSource = require('../../assets/tabs-explainer.jpg')
export const CLOUD_SPLASH: ImageRequireSource = require('../../assets/cloud-splash.png')
export const DEF_AVATAR: ImageRequireSource = require('../../assets/default-avatar.png')
export const CLOUD_SPLASH: ImageRequireSource = require('../../assets/splash.png')
+2 -6
View File
@@ -1,10 +1,6 @@
import {ImageRequireSource} from 'react-native'
// @ts-ignore we need to pretend -prf
export const DEF_AVATAR: ImageRequireSource = {uri: '/img/default-avatar.jpg'}
export const DEF_AVATAR: ImageRequireSource = {uri: '/img/default-avatar.png'}
// @ts-ignore we need to pretend -prf
export const TABS_EXPLAINER: ImageRequireSource = {
uri: '/img/tabs-explainer.jpg',
}
// @ts-ignore we need to pretend -prf
export const CLOUD_SPLASH: ImageRequireSource = {uri: '/img/cloud-splash.png'}
export const CLOUD_SPLASH: ImageRequireSource = {uri: '/img/splash.png'}
+25
View File
@@ -0,0 +1,25 @@
export interface AccumulateResponse<T> {
cursor?: string
items: T[]
}
export type AccumulateFetchFn<T> = (
cursor: string | undefined,
) => Promise<AccumulateResponse<T>>
export async function accumulate<T>(
fn: AccumulateFetchFn<T>,
pageLimit = 100,
): Promise<T[]> {
let cursor: string | undefined
let acc: T[] = []
for (let i = 0; i < pageLimit; i++) {
const res = await fn(cursor)
cursor = res.cursor
acc = acc.concat(res.items)
if (!cursor) {
break
}
}
return acc
}
-68
View File
@@ -1,68 +0,0 @@
import {runInAction} from 'mobx'
import {deepObserve} from 'mobx-utils'
import set from 'lodash.set'
const ongoingActions = new Set<any>()
/**
* This is a TypeScript function that optimistically updates data on the client-side before sending a
* request to the server and rolling back changes if the request fails.
* @param {T} model - The object or record that needs to be updated optimistically.
* @param preUpdate - `preUpdate` is a function that is called before the server update is executed. It
* can be used to perform any necessary actions or updates on the model or UI before the server update
* is initiated.
* @param serverUpdate - `serverUpdate` is a function that returns a Promise representing the server
* update operation. This function is called after the previous state of the model has been recorded
* and the `preUpdate` function has been executed. If the server update is successful, the `postUpdate`
* function is called with the result
* @param [postUpdate] - `postUpdate` is an optional callback function that will be called after the
* server update is successful. It takes in the response from the server update as its parameter. If
* this parameter is not provided, nothing will happen after the server update.
* @returns A Promise that resolves to `void`.
*/
export const updateDataOptimistically = async <
T extends Record<string, any>,
U,
>(
model: T,
preUpdate: () => void,
serverUpdate: () => Promise<U>,
postUpdate?: (res: U) => void,
): Promise<void> => {
if (ongoingActions.has(model)) {
return
}
ongoingActions.add(model)
const prevState: Map<string, any> = new Map<string, any>()
const dispose = deepObserve(model, (change, path) => {
if (change.observableKind === 'object') {
if (change.type === 'update') {
prevState.set(
[path, change.name].filter(Boolean).join('.'),
change.oldValue,
)
} else if (change.type === 'add') {
prevState.set([path, change.name].filter(Boolean).join('.'), undefined)
}
}
})
preUpdate()
dispose()
try {
const res = await serverUpdate()
runInAction(() => {
postUpdate?.(res)
})
} catch (error) {
runInAction(() => {
prevState.forEach((value, path) => {
set(model, path, value)
})
})
throw error
} finally {
ongoingActions.delete(model)
}
}
+24
View File
@@ -0,0 +1,24 @@
import {timeout} from './timeout'
export async function until(
retries: number,
delay: number,
cond: (v: any, err: any) => boolean,
fn: () => Promise<any>,
): Promise<boolean> {
while (retries > 0) {
try {
const v = await fn()
if (cond(v, undefined)) {
return true
}
} catch (e: any) {
if (cond(undefined, e)) {
return true
}
}
await timeout(delay)
retries--
}
return false
}
+1
View File
@@ -0,0 +1 @@
export {unstable_batchedUpdates as batchedUpdates} from 'react-native'
+2
View File
@@ -0,0 +1,2 @@
// @ts-ignore
export {unstable_batchedUpdates as batchedUpdates} from 'react-dom'
+2
View File
@@ -0,0 +1,2 @@
import Stub from '#/lib/broadcast/stub'
export default Stub
+2
View File
@@ -0,0 +1,2 @@
import Stub from '#/lib/broadcast/stub'
export default 'BroadcastChannel' in window ? window.BroadcastChannel : Stub
+11
View File
@@ -0,0 +1,11 @@
export default class BroadcastChannel {
constructor(public name: string) {}
postMessage(_data: any) {}
close() {}
onmessage: (event: MessageEvent) => void = () => {}
addEventListener(_type: string, _listener: (event: MessageEvent) => void) {}
removeEventListener(
_type: string,
_listener: (event: MessageEvent) => void,
) {}
}
+1
View File
@@ -1 +1,2 @@
export const LOGIN_INCLUDE_DEV_SERVERS = true
export const PWI_ENABLED = true
+13 -4
View File
@@ -1,4 +1,10 @@
import {Insets} from 'react-native'
import {Insets, Platform} from 'react-native'
export const LOCAL_DEV_SERVICE =
Platform.OS === 'android' ? 'http://10.0.2.2:2583' : 'http://localhost:2583'
export const STAGING_SERVICE = 'https://staging.bsky.dev'
export const PROD_SERVICE = 'https://bsky.social'
export const DEFAULT_SERVICE = PROD_SERVICE
const HELP_DESK_LANG = 'en-us'
export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}`
@@ -43,7 +49,10 @@ export function IS_PROD(url: string) {
// until open federation, "production" is defined as the main server
// this definition will not work once federation is enabled!
// -prf
return url.startsWith('https://bsky.social')
return (
url.startsWith('https://bsky.social') ||
url.startsWith('https://api.bsky.app')
)
}
export const PROD_TEAM_HANDLES = [
@@ -107,8 +116,8 @@ export async function DEFAULT_FEEDS(
} else {
// production
return {
pinned: [PROD_DEFAULT_FEED('whats-hot')],
saved: [PROD_DEFAULT_FEED('whats-hot')],
pinned: [],
saved: [],
}
}
}
+47 -30
View File
@@ -1,41 +1,58 @@
import {useCallback, useState} from 'react'
import {useStores} from 'state/index'
import {useAnalytics} from 'lib/analytics/analytics'
import {StackActions, useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
import {AccountData} from 'state/models/session'
import {reset as resetNavigation} from '../../Navigation'
import * as Toast from 'view/com/util/Toast'
import {useCallback} from 'react'
export function useAccountSwitcher(): [
boolean,
(v: boolean) => void,
(acct: AccountData) => Promise<void>,
] {
import {isWeb} from '#/platform/detection'
import {useAnalytics} from '#/lib/analytics/analytics'
import {useSessionApi, SessionAccount} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {useCloseAllActiveElements} from '#/state/util'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
export function useAccountSwitcher() {
const {track} = useAnalytics()
const store = useStores()
const [isSwitching, setIsSwitching] = useState(false)
const navigation = useNavigation<NavigationProp>()
const {selectAccount, clearCurrentAccount} = useSessionApi()
const closeAllActiveElements = useCloseAllActiveElements()
const {requestSwitchToAccount} = useLoggedOutViewControls()
const onPressSwitchAccount = useCallback(
async (acct: AccountData) => {
async (account: SessionAccount) => {
track('Settings:SwitchAccountButtonClicked')
setIsSwitching(true)
const success = await store.session.resumeSession(acct)
store.shell.closeAllActiveElements()
if (success) {
resetNavigation()
Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
} else {
try {
if (account.accessJwt) {
closeAllActiveElements()
if (isWeb) {
// We're switching accounts, which remounts the entire app.
// On mobile, this gets us Home, but on the web we also need reset the URL.
// We can't change the URL via a navigate() call because the navigator
// itself is about to unmount, and it calls pushState() too late.
// So we change the URL ourselves. The navigator will pick it up on remount.
history.pushState(null, '', '/')
}
await selectAccount(account)
setTimeout(() => {
Toast.show(`Signed in as @${account.handle}`)
}, 100)
} else {
closeAllActiveElements()
requestSwitchToAccount({requestedAccount: account.did})
Toast.show(
`Please sign in as @${account.handle}`,
'circle-exclamation',
)
}
} catch (e) {
Toast.show('Sorry! We need you to enter your password.')
navigation.navigate('HomeTab')
navigation.dispatch(StackActions.popToTop())
store.session.clear()
clearCurrentAccount() // back user out to login
}
},
[track, setIsSwitching, navigation, store],
[
track,
clearCurrentAccount,
selectAccount,
closeAllActiveElements,
requestSwitchToAccount,
],
)
return [isSwitching, setIsSwitching, onPressSwitchAccount]
return {onPressSwitchAccount}
}
@@ -0,0 +1,15 @@
// Be warned. This Hook is very buggy unless used in a very constrained way.
// To use it safely:
//
// - DO NOT pass its return value as a prop to any user-defined component.
// - DO NOT pass its return value to more than a single component.
//
// In other words, the only safe way to use it is next to the leaf Reanimated View.
//
// Relevant bug reports:
// - https://github.com/software-mansion/react-native-reanimated/issues/5345
// - https://github.com/software-mansion/react-native-reanimated/issues/5360
// - https://github.com/software-mansion/react-native-reanimated/issues/5364
//
// It's great when it works though.
export {useAnimatedScrollHandler} from 'react-native-reanimated'
@@ -0,0 +1,44 @@
import {useRef, useEffect} from 'react'
import {useAnimatedScrollHandler as useAnimatedScrollHandler_BUGGY} from 'react-native-reanimated'
export const useAnimatedScrollHandler: typeof useAnimatedScrollHandler_BUGGY = (
config,
deps,
) => {
const ref = useRef(config)
useEffect(() => {
ref.current = config
})
return useAnimatedScrollHandler_BUGGY(
{
onBeginDrag(e, ctx) {
if (typeof ref.current !== 'function' && ref.current.onBeginDrag) {
ref.current.onBeginDrag(e, ctx)
}
},
onEndDrag(e, ctx) {
if (typeof ref.current !== 'function' && ref.current.onEndDrag) {
ref.current.onEndDrag(e, ctx)
}
},
onMomentumBegin(e, ctx) {
if (typeof ref.current !== 'function' && ref.current.onMomentumBegin) {
ref.current.onMomentumBegin(e, ctx)
}
},
onMomentumEnd(e, ctx) {
if (typeof ref.current !== 'function' && ref.current.onMomentumEnd) {
ref.current.onMomentumEnd(e, ctx)
}
},
onScroll(e, ctx) {
if (typeof ref.current === 'function') {
ref.current(e, ctx)
} else if (ref.current.onScroll) {
ref.current.onScroll(e, ctx)
}
},
},
deps,
)
}
-27
View File
@@ -1,27 +0,0 @@
import {useEffect, useState} from 'react'
import {useStores} from 'state/index'
import {CustomFeedModel} from 'state/models/feeds/custom-feed'
export function useCustomFeed(uri: string): CustomFeedModel | undefined {
const store = useStores()
const [item, setItem] = useState<CustomFeedModel | undefined>()
useEffect(() => {
async function fetchView() {
const res = await store.agent.app.bsky.feed.getFeedGenerator({
feed: uri,
})
const view = res.data.view
return view
}
async function buildFeedItem() {
const view = await fetchView()
if (view) {
const temp = new CustomFeedModel(store, view)
setItem(temp)
}
}
buildFeedItem()
}, [store, uri])
return item
}
-54
View File
@@ -1,54 +0,0 @@
import React from 'react'
import {AppBskyActorDefs} from '@atproto/api'
import {useStores} from 'state/index'
import {FollowState} from 'state/models/cache/my-follows'
export function useFollowProfile(profile: AppBskyActorDefs.ProfileViewBasic) {
const store = useStores()
const state = store.me.follows.getFollowState(profile.did)
return {
state,
following: state === FollowState.Following,
toggle: React.useCallback(async () => {
if (state === FollowState.Following) {
try {
await store.agent.deleteFollow(
store.me.follows.getFollowUri(profile.did),
)
store.me.follows.removeFollow(profile.did)
return {
state: FollowState.NotFollowing,
following: false,
}
} catch (e: any) {
store.log.error('Failed to delete follow', e)
throw e
}
} else if (state === FollowState.NotFollowing) {
try {
const res = await store.agent.follow(profile.did)
store.me.follows.addFollow(profile.did, {
followRecordUri: res.uri,
did: profile.did,
handle: profile.handle,
displayName: profile.displayName,
avatar: profile.avatar,
})
return {
state: FollowState.Following,
following: true,
}
} catch (e: any) {
store.log.error('Failed to create follow', e)
throw e
}
}
return {
state: FollowState.Unknown,
following: false,
}
}, [store, profile, state]),
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ export function useIsKeyboardVisible({
const [isKeyboardVisible, setKeyboardVisible] = useState(false)
// NOTE
// only iOS suppose the "will" events
// only iOS supports the "will" events
// -prf
const showEvent =
isIOS && iosUseWillEvents ? 'keyboardWillShow' : 'keyboardDidShow'
+19 -34
View File
@@ -1,57 +1,42 @@
import React from 'react'
import {autorun} from 'mobx'
import {useStores} from 'state/index'
import {
Easing,
interpolate,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated'
import {interpolate, useAnimatedStyle} from 'react-native-reanimated'
import {useMinimalShellMode as useMinimalShellModeState} from '#/state/shell/minimal-mode'
import {useShellLayout} from '#/state/shell/shell-layout'
export function useMinimalShellMode() {
const store = useStores()
const minimalShellInterp = useSharedValue(0)
const mode = useMinimalShellModeState()
const {footerHeight, headerHeight} = useShellLayout()
const footerMinimalShellTransform = useAnimatedStyle(() => {
return {
opacity: interpolate(minimalShellInterp.value, [0, 1], [1, 0]),
pointerEvents: mode.value === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - mode.value, 2),
transform: [
{translateY: interpolate(minimalShellInterp.value, [0, 1], [0, 25])},
{
translateY: interpolate(mode.value, [0, 1], [0, footerHeight.value]),
},
],
}
})
const headerMinimalShellTransform = useAnimatedStyle(() => {
return {
opacity: interpolate(minimalShellInterp.value, [0, 1], [1, 0]),
pointerEvents: mode.value === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - mode.value, 2),
transform: [
{translateY: interpolate(minimalShellInterp.value, [0, 1], [0, -25])},
{
translateY: interpolate(mode.value, [0, 1], [0, -headerHeight.value]),
},
],
}
})
const fabMinimalShellTransform = useAnimatedStyle(() => {
return {
transform: [
{translateY: interpolate(minimalShellInterp.value, [0, 1], [-44, 0])},
{
translateY: interpolate(mode.value, [0, 1], [-44, 0]),
},
],
}
})
React.useEffect(() => {
return autorun(() => {
if (store.shell.minimalShellMode) {
minimalShellInterp.value = withTiming(1, {
duration: 125,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
})
} else {
minimalShellInterp.value = withTiming(0, {
duration: 125,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
})
}
})
}, [minimalShellInterp, store.shell.minimalShellMode])
return {
footerMinimalShellTransform,
headerMinimalShellTransform,
+23
View File
@@ -0,0 +1,23 @@
import {useCallback, useInsertionEffect, useRef} from 'react'
// This should be used sparingly. It erases reactivity, i.e. when the inputs
// change, the function itself will remain the same. This means that if you
// use this at a higher level of your tree, and then some state you read in it
// changes, there is no mechanism for anything below in the tree to "react"
// to this change (e.g. by knowing to call your function again).
//
// Also, you should avoid calling the returned function during rendering
// since the values captured by it are going to lag behind.
export function useNonReactiveCallback<T extends Function>(fn: T): T {
const ref = useRef(fn)
useInsertionEffect(() => {
ref.current = fn
}, [fn])
return useCallback(
(...args: any) => {
const latestFn = ref.current
return latestFn(...args)
},
[ref],
) as unknown as T
}
+18 -17
View File
@@ -1,27 +1,28 @@
import * as Updates from 'expo-updates'
import {useCallback, useEffect} from 'react'
import {AppState} from 'react-native'
import {useStores} from 'state/index'
import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {t} from '@lingui/macro'
export function useOTAUpdate() {
const store = useStores()
const {openModal} = useModalControls()
// HELPER FUNCTIONS
const showUpdatePopup = useCallback(() => {
store.shell.openModal({
openModal({
name: 'confirm',
title: 'Update Available',
message:
'A new version of the app is available. Please update to continue using the app.',
title: t`Update Available`,
message: t`A new version of the app is available. Please update to continue using the app.`,
onPressConfirm: async () => {
Updates.reloadAsync().catch(err => {
throw err
})
},
})
}, [store.shell])
}, [openModal])
const checkForUpdate = useCallback(async () => {
store.log.debug('useOTAUpdate: Checking for update...')
logger.debug('useOTAUpdate: Checking for update...')
try {
// Check if new OTA update is available
const update = await Updates.checkForUpdateAsync()
@@ -34,18 +35,18 @@ export function useOTAUpdate() {
// show a popup modal
showUpdatePopup()
} catch (e) {
console.error('useOTAUpdate: Error while checking for update', e)
store.log.error('useOTAUpdate: Error while checking for update', e)
logger.error('useOTAUpdate: Error while checking for update', {
error: e,
})
}
}, [showUpdatePopup, store.log])
}, [showUpdatePopup])
const updateEventListener = useCallback(
(event: Updates.UpdateEvent) => {
store.log.debug('useOTAUpdate: Listening for update...')
logger.debug('useOTAUpdate: Listening for update...')
if (event.type === Updates.UpdateEventType.ERROR) {
store.log.error(
'useOTAUpdate: Error while listening for update',
event.message,
)
logger.error('useOTAUpdate: Error while listening for update', {
message: event.message,
})
} else if (event.type === Updates.UpdateEventType.NO_UPDATE_AVAILABLE) {
// Handle no update available
// do nothing
@@ -55,7 +56,7 @@ export function useOTAUpdate() {
showUpdatePopup()
}
},
[showUpdatePopup, store.log],
[showUpdatePopup],
)
useEffect(() => {
-66
View File
@@ -1,66 +0,0 @@
import {useState, useCallback, useRef} from 'react'
import {NativeSyntheticEvent, NativeScrollEvent} from 'react-native'
import {RootStoreModel} from 'state/index'
import {s} from 'lib/styles'
import {useWebMediaQueries} from './useWebMediaQueries'
const Y_LIMIT = 10
const useDeviceLimits = () => {
const {isDesktop} = useWebMediaQueries()
return {
dyLimitUp: isDesktop ? 30 : 10,
dyLimitDown: isDesktop ? 150 : 10,
}
}
export type OnScrollCb = (
event: NativeSyntheticEvent<NativeScrollEvent>,
) => void
export type ResetCb = () => void
export function useOnMainScroll(
store: RootStoreModel,
): [OnScrollCb, boolean, ResetCb] {
let lastY = useRef(0)
let [isScrolledDown, setIsScrolledDown] = useState(false)
const {dyLimitUp, dyLimitDown} = useDeviceLimits()
return [
useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
const y = event.nativeEvent.contentOffset.y
const dy = y - (lastY.current || 0)
lastY.current = y
if (!store.shell.minimalShellMode && dy > dyLimitDown && y > Y_LIMIT) {
store.shell.setMinimalShellMode(true)
} else if (
store.shell.minimalShellMode &&
(dy < dyLimitUp * -1 || y <= Y_LIMIT)
) {
store.shell.setMinimalShellMode(false)
}
if (
!isScrolledDown &&
event.nativeEvent.contentOffset.y > s.window.height
) {
setIsScrolledDown(true)
} else if (
isScrolledDown &&
event.nativeEvent.contentOffset.y < s.window.height
) {
setIsScrolledDown(false)
}
},
[store.shell, dyLimitDown, dyLimitUp, isScrolledDown],
),
isScrolledDown,
useCallback(() => {
setIsScrolledDown(false)
store.shell.setMinimalShellMode(false)
lastY.current = 1e8 // NOTE we set this very high so that the onScroll logic works right -prf
}, [store, setIsScrolledDown]),
]
}
+38 -34
View File
@@ -1,3 +1,4 @@
import {useMemo} from 'react'
import {TextStyle, ViewStyle} from 'react-native'
import {useTheme, PaletteColorName, PaletteColor} from '../ThemeContext'
@@ -15,38 +16,41 @@ export interface UsePaletteValue {
icon: TextStyle
}
export function usePalette(color: PaletteColorName): UsePaletteValue {
const palette = useTheme().palette[color]
return {
colors: palette,
view: {
backgroundColor: palette.background,
},
viewLight: {
backgroundColor: palette.backgroundLight,
},
btn: {
backgroundColor: palette.backgroundLight,
},
border: {
borderColor: palette.border,
},
borderDark: {
borderColor: palette.borderDark,
},
text: {
color: palette.text,
},
textLight: {
color: palette.textLight,
},
textInverted: {
color: palette.textInverted,
},
link: {
color: palette.link,
},
icon: {
color: palette.icon,
},
}
const theme = useTheme()
return useMemo(() => {
const palette = theme.palette[color]
return {
colors: palette,
view: {
backgroundColor: palette.background,
},
viewLight: {
backgroundColor: palette.backgroundLight,
},
btn: {
backgroundColor: palette.backgroundLight,
},
border: {
borderColor: palette.border,
},
borderDark: {
borderColor: palette.borderDark,
},
text: {
color: palette.text,
},
textLight: {
color: palette.textLight,
},
textInverted: {
color: palette.textInverted,
},
link: {
color: palette.link,
},
icon: {
color: palette.icon,
},
}
}, [theme, color])
}
+4 -8
View File
@@ -3,18 +3,14 @@ import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
import {bskyTitle} from 'lib/strings/headings'
import {useStores} from 'state/index'
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
/**
* Requires consuming component to be wrapped in `observer`:
* https://stackoverflow.com/a/71488009
*/
export function useSetTitle(title?: string) {
const navigation = useNavigation<NavigationProp>()
const {unreadCountLabel} = useStores().me.notifications
const numUnread = useUnreadNotifications()
useEffect(() => {
if (title) {
navigation.setOptions({title: bskyTitle(title, unreadCountLabel)})
navigation.setOptions({title: bskyTitle(title, numUnread)})
}
}, [title, navigation, unreadCountLabel])
}, [title, navigation, numUnread])
}

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