Compare commits

...

610 Commits

Author SHA1 Message Date
Dan Abramov f2e52a1cd7 Patch facebook/metro#1130 to profile module init 2023-11-02 18:40:44 +00:00
dan f9944b55e2 Simplify loading emoji data (#1790) 2023-11-01 17:49:07 +00:00
dan 9fb20517ae Remove unused Intl.Segmenter polyfill (#1789) 2023-11-01 16:52:34 +00:00
Eric Bailey 4bf1719092 Merge pull request #1788 from bluesky-social/eric/app-899-remove-icons-from-startup-code
Revise icons startup code
2023-11-01 10:38:37 -05:00
dan 85c26fb5a8 Instrument module init in DEV (#1787) 2023-11-01 15:31:33 +00:00
Eric Bailey b42806a6e0 Revise icons startup code 2023-11-01 10:15:37 -05:00
Samiksha Mishra adb5ce2961 Updated the mistake (#1777)
Corrected the typo error
2023-10-31 08:39:35 -07:00
Tarık b5f36dc7a3 UI/UX improvements in Edit profile screen (#1767)
* Remove cropped area in Edit profile screen iOS

* Hide cancel button when saving changes in EditProfile

* Disable fadeOut animation cancel button for web in EditProfile screen

Since react-native-reanimated is not configured for web support (https://docs.swmansion.com/react-native-reanimated/docs/2.x/fundamentals/web-support/), we are enabling fade out animation for iOS and android solely
2023-10-30 21:01:25 -07:00
Igor Shevchenko b2d4771b5f chore: fix typos in testing.md (#1764) 2023-10-30 20:40:30 -07:00
Eric Bailey ddf0a1cced Filter non-self threads from profile view (#1757) 2023-10-30 20:40:06 -07:00
Paul Frazee 40752982da Merge branch 'main' of https://github.com/haideralipunjabi/social-app into main 2023-10-30 19:26:23 -07:00
Leo Lamprecht 68c809b09a Use more beautiful title separator (#1728) 2023-10-30 19:25:19 -07:00
Pouria Delfanazari 21f7a2a247 Allow back navigation to home on profile header (#1717)
Navigate back to home if there is no navigation history.
2023-10-30 19:23:41 -07:00
Paul Frazee 97ce9a73d2 Merge branch 'main' of github.com:bluesky-social/social-app into main 2023-10-30 19:18:34 -07:00
Paul Frazee 33ea9ccef7 Merge branch 'Tanza3D-improve-post-button' into main 2023-10-30 19:18:24 -07:00
Paul Frazee 33a87ca22e Yep, one pixel 2023-10-30 19:18:02 -07:00
dan ef1d6ee490 Log time to first paint (#1775)
* Log time to first paint

* Fix TS
2023-10-31 02:07:14 +00:00
Paul Frazee 1eb623ee2f Merge branch 'improve-post-button' of https://github.com/Tanza3D/social-app into Tanza3D-improve-post-button 2023-10-30 18:54:29 -07:00
Paul Frazee d5c7b8ceb8 Merge branch 'main' of github.com:bluesky-social/social-app into main 2023-10-30 18:07:33 -07:00
Paul Frazee bae429202a Merge branch 'liby-fix/step2-waitlist-link-accessibility' into main 2023-10-30 18:06:58 -07:00
Paul Frazee 0747193977 Fix a11y prop 2023-10-30 18:06:31 -07:00
dan 4a4106ba45 Don't inline-require Babel runtime helpers (#1774) 2023-10-31 00:59:00 +00:00
Paul Frazee 2577cbf252 Merge branch 'fix/step2-waitlist-link-accessibility' of https://github.com/liby/social-app into liby-fix/step2-waitlist-link-accessibility 2023-10-30 17:50:44 -07:00
Paul Frazee 9f33badfff Merge branch 'main' of github.com:bluesky-social/social-app into main 2023-10-30 17:50:31 -07:00
Paul Frazee e18e7ce34a Merge branch 'liby-bugfix/desktop-web-text-link-media-query-issue' into main 2023-10-30 17:45:07 -07:00
Paul Frazee 9826e7c7b9 Rename component to match new behavior 2023-10-30 17:44:58 -07:00
dan 515c9d5529 Don't compile Hermes-supported features with Babel (#1773)
* Don't transpile Hermes-supported features

* Disable the object spread plugin

We're using Hermes, and Hermes supports object spread.
We can remove this patch when we upgrade Expo because it'll be conditional there.
2023-10-31 00:23:01 +00:00
Paul Frazee 871ded193c Merge branch 'bugfix/desktop-web-text-link-media-query-issue' of https://github.com/liby/social-app into liby-bugfix/desktop-web-text-link-media-query-issue 2023-10-30 17:11:17 -07:00
Paul Frazee e1dcedd87e Merge branch 'elijah-wright-patch-1' into main 2023-10-30 17:08:15 -07:00
Paul Frazee 1acf4969c5 Fix lint 2023-10-30 17:08:07 -07:00
Paul Frazee dbbd71c6d0 Merge branch 'patch-1' of https://github.com/elijah-wright/social-app into elijah-wright-patch-1 2023-10-30 17:03:34 -07:00
dan 84ee64025f Let Metro handle import/export instead of Babel (#1772)
* Let Metro handle import/export instead of Babel

* Keep using Babel for ESM->CJS in tests
2023-10-30 21:54:23 +00:00
Paul Frazee 6c11c0b81d 1.55 2023-10-30 09:58:58 -07:00
dan 7ebf1ed371 Initialize screen modules lazily (#1763)
* Initialize screen modules lazily

* Use getComponent (thanks @ecreeth for the tip)
2023-10-27 19:06:28 +01:00
dan 53afafb04f Remove redundant linking listeners (#1760) 2023-10-26 21:05:48 -07:00
dan 3e5a64b454 Fix white flash on startup (#1759) 2023-10-27 04:06:40 +01:00
dan 46f3265936 Enable inline requires (#1756) 2023-10-26 17:54:15 -07:00
Paul Frazee a1a61ef2e5 Merge branch 'main' of github.com:bluesky-social/social-app into main 2023-10-26 10:40:35 -07:00
Ansh 927cee18d9 Update Analytics (#1743) 2023-10-26 08:37:14 -07:00
Haider Ali Punjabi 7fa0708e0e Added show more for long posts 2023-10-26 19:38:39 +05:30
Ansh c13b6946ba 1721 Update support link (#1722)
* change support page copy

* fix grammar
2023-10-25 10:00:46 -07:00
Ansh ee80cae75f add scroll indicator on web to profile suggested follows card (#1724) 2023-10-25 09:54:52 -07:00
Ansh 1781b3aab7 allow enter key to trigger waitlist signup (#1723) 2023-10-25 09:54:04 -07:00
elliot 3426b24f67 Fixed CropImage modal aspect ratio buttons visibility in dark mode (#1730) 2023-10-25 09:41:56 -07:00
dan 4a4be1de1a Remove MobX from Link (#1745)
* Remove MobX from Link

* Keep memo() though

We previously had observer() apply it automatically, so re-add it
2023-10-25 08:47:21 -07:00
dan 32fbb9dba7 Simplify the avatar component (#1744)
* Copypaste UserAvatar to EditableUserAvatar

* Swap usages with onSelectNewAvatar to EditableUserAvatar

* Split prop types into UserAvatarProps and EditableUserAvatarProps

* Remove dead branches from EditableUserAvatar

* Remove dead branches from UserAvatar
2023-10-25 04:57:34 +01:00
dan c6e28f88e5 Fix four gallery images for Firefox (#1710) 2023-10-18 15:08:12 -04:00
Tanza3D db0de651ba Fix wonky comment 2023-10-18 14:14:45 +01:00
Tanza3D 6848ade046 Shrink to 16 again 2023-10-18 13:53:55 +01:00
Tanza3D de94acb6c8 Improve New Post button 2023-10-18 13:53:00 +01:00
Bryan Lee 78b374b43d Improved accessibility and interaction for waitlist Link in Step2 component 2023-10-15 06:26:56 +08:00
Paul Frazee 01e25c912c 1.54 2023-10-14 10:32:11 -07:00
Ansh 8e9cf182c2 Performance optimization (#1676)
* upgrade sentry to support profiling monitoring

* remove console logs in production builds

* feeds tab bar and bottom bar animation centralized

* refactor FeedPage out of Home

* add script to start in production mode

* move FAB inner to reanimated

* move FABInner back to `Animated` RN animation

* add perf commands

* add testing with Maestro and perf with Flashlight

* fix merge conflicts

* fix resourceClass name in eas.json

* fix onEndReachedThreshold in Feed

* memoize styles

* go back to old styling for LoadLatestBtn

* remove reanimated code from useMinimalShellMode

* move shell animations to hook/reanimated for perf

* fix empty state issue

* make shell animation feel smoother

* make shell animation more smooth

* run animation with autorun

* specify keys for tab bar properly

* remove comments

* remove already imported dep

* fix lint

* add testing instructions

* mock sentry-expo for jest

* fix jest mocks

* Fix the load-latest button on desktop and tablet

* Fix: don't move the FAB in tablet mode

* Fix type error

* Fix tabs bar positioning on tablet

* Fix types

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-10-13 18:54:35 -07:00
dan 9042f503c2 Scope drawer drag workaround to feed pagers (#1694) 2023-10-13 15:15:36 -07:00
dan abfd9a8c0b Toggle lightbox controls on tap (#1687)
* Make the lightbox controls animation smoother

* Toggle controls on tap

* Disable pointer events when hidden
2023-10-13 20:10:27 +01:00
dan f447eaa669 Fix profile preview jump (#1693)
* Add top inset for profile preview to match target screen

* Avoid flicker by waiting for profile screen navigation

* Fix glimmer to align with the content

* A more reliable (but non-scientific) fix for the flash

* Lower the timeout
2023-10-13 20:10:15 +01:00
dan d5ccbd76d5 Disable events on hidden bars (#1686) 2023-10-13 15:29:50 +01:00
dan eba9f8a166 Fix profile layout shift (#1690) 2023-10-13 15:25:16 +01:00
dan 2a1edab6d4 Don't re-render bars when showing/hiding them (#1691)
* Don't re-render bars when showing/hiding them

* Fix more cases

* Use autorun instead of reaction to fix first render
2023-10-13 15:24:28 +01:00
dan 4431cfe2d2 Fix crash when scrolling down on the web (#1684) 2023-10-12 13:15:19 -07:00
dan 997918547c Make shell hide/show animation smoother (#1683)
* Make shell hide/show animation smoother

* Also animate "load latest"
2023-10-12 21:02:17 +01:00
dan c2a1cf4e56 Fix layout shift for multi-image posts (#1673)
* Fix layout shift for multi-image posts

* Add a comment for the hack

* Use margins instead of gap

* Move alt label
2023-10-12 19:14:27 +01:00
Jake Gold d98e3a8b45 bskyweb: add rate limiting to reduce DoSability 2023-10-11 13:00:59 -07:00
Eric Bailey d40d931d86 use new zeed-dom version (#1671) 2023-10-11 09:04:06 -07:00
Paul Frazee 5dd673d1ff 1.53 2023-10-10 17:32:19 -07:00
Eric Bailey 10deddd0ad resolve fork of zeed-dom (#1663) 2023-10-10 16:23:12 -07:00
dan a48c213af5 Port remaining lightbox code to Reanimated (#1669)
* Port remaining lightbox code to Reanimated

* Fix  memoization
2023-10-10 23:48:31 +01:00
Paul Frazee 0b44af38ea Update testrunner to use new dev-env [WIP] (#1575)
* Update testrunner to use new dev-env

* Fix label testcase

* Vendor the dev-infra scripts from the atproto repo for the dev-env server runner

* Bump detox to fix the ios sim control issue

* Use iphone 15 pro for tests

* Ensure the reminders never trigger during tests

* Skip the shell tests due to a crash bug with detox and the drawer
2023-10-10 15:46:27 -07:00
dan aad8d12ede Fix MobX crash for Android lightbox (#1668)
* Fix MobX crash for Android lightbox

* Reorder props to match between forks
2023-10-10 23:02:00 +01:00
dan 209d8b683c Change lightbox to use Pager (#1666)
* Change lightbox to use Pager

* Fix crash issue on ios

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-10-10 22:37:28 +01:00
Ansh aa085b0b14 make empty feed required (#1667) 2023-10-10 14:32:00 -07:00
Paul Frazee 4d450da194 Only warn on links to bsky.app if it represents itself as another url (#1662)
* Only warn on links to bsky.app if it represents itself as another url (close #1652)

* Clean up
2023-10-10 14:02:50 -07:00
Paul Frazee e878da04a1 Fix keyboard double pad issue in email change & verify modals (#1664) 2023-10-10 14:02:31 -07:00
Eric Bailey fc28fc639f Don't highlight tags in composer yet (#1665) 2023-10-10 13:44:27 -07:00
Paul Frazee d68b4ca856 Fix: fetch follows on desktop search for typeahead (#1660) 2023-10-10 12:27:32 -07:00
dan 60c0db03f7 Remove duplicate modal container (#1661) 2023-10-10 11:57:14 -07:00
dan d47ff542da Drive-by lightbox refactors (#1659)
* Remove dead code from lightbox

* Rename imageIndex prop to initialImageIndex

* Rename currentImageIndex to imageIndex
2023-10-10 11:07:21 -07:00
Paul Frazee bc2c44cb98 Only prompt users once to verify email (according to local storage) close #1657 (#1658) 2023-10-10 10:57:09 -07:00
Paul Frazee 098f4b526e Revert "Fix invite codes flash on desktop, use loading placeholder (#1591)" (#1656)
This reverts commit 9278822088.
2023-10-10 10:08:04 -07:00
dan f452ce74f4 Refactor iOS lightbox to Reanimated (#1645)
* Remove unnecessary transform logic

* Switch iOS swipe-to-dimiss to Reanimated
2023-10-10 10:04:38 +01:00
Paul Frazee 832b05b64a Bump package.json to 1.52 2023-10-09 15:24:23 -07:00
Paul Frazee acb003d7e1 Merge branch 'main' of github.com:bluesky-social/social-app into main 2023-10-09 14:58:40 -07:00
dan ce0f796008 Remove unnecessary opacity logic (#1646) 2023-10-09 22:29:56 +01:00
Muneyuki Noguchi b12cfbe90b Fix typo in image.ts (#1638) 2023-10-09 12:29:16 -07:00
Raku d85bbc1a28 fix typo README.md (#1631) 2023-10-09 11:55:15 -07:00
Toni Alatalo c43a0d8160 Typo fix in README.md: "small about" -> "small amount" (#1639) 2023-10-09 11:48:57 -07:00
Elijah Wright c39352f859 readd hint 2023-10-08 14:30:57 -07:00
Elijah Wright 0122cdb0b6 fix accessibility in AutoSizedImage 2023-10-08 14:29:20 -07:00
Paul Frazee 9aff2597cf Bump build revision 2023-10-06 18:02:22 -07:00
Paul Frazee 77b21dfb1c Fix bug that prevents unfollowing (#1633) 2023-10-06 18:01:57 -07:00
Paul Frazee 9cff82448f 1.52 2023-10-06 08:48:39 -07:00
bnewbold 34f9d12987 README: tweaks to high-level context (#1625) 2023-10-06 08:45:41 -07:00
dan d2f11f8e36 Fix stuck lightbox header after double tap (#1627) 2023-10-05 20:22:32 -07:00
Paul Frazee 3b78d3227f Fix: add padding to the spinner bottom while loading threads (#1626) 2023-10-05 20:10:43 -07:00
dan 64153067e3 Rewrite Android lightbox (#1624) 2023-10-05 19:54:36 -07:00
Paul Frazee 8366fe2c4a Dont trim before posting (close #1621) (#1622)
* Dont trim before posting (close #1621)

* Tweak: do trim end
2023-10-05 19:08:20 -07:00
Paul Frazee b74a0e0fe2 Only listen to back button on android (#1623) 2023-10-05 18:29:41 -07:00
Paul Frazee bd7db8af26 Improve typeahead search with inclusion of followed users (temporary solution) (#1612)
* Update follows cache to maintain some user info

* Prioritize follows in composer autocomplete

* Clean up logic and add new autocomplete to search

* Update follow hook
2023-10-05 16:44:05 -07:00
Paul Frazee 19f8389fc7 Slightly smaller highlighted post text (#1608) 2023-10-05 16:13:49 -07:00
Paul Frazee 0168aada37 Pull upstream bugfixes to bottom-sheet (#1606)
* Bump bottom-sheet to latest for bugfixes

* Small tweak to the account switcher
2023-10-05 15:55:31 -07:00
dan 4ec5fabdd1 Fix animations and gestures getting reset on state updates in the lightbox (#1618)
* Fix translation resetting on state update

* Copy getImageStyles into iOS and Android forks

* Fix opacity resetting on state update
2023-10-05 15:52:04 -07:00
dan 260b03a05c Remove unused lightbox options (#1616)
* Inline lightbox helpers

* Delete unused useImagePrefetch

* Delete unused long press gesture

* Always enable double tap

* Always enable swipe to close

* Remove unused onImageIndexChange

* Inline custom Hooks into ImageViewing

* Declare LightboxFooter outside Lightbox

* Add more TODO comments

* Inline useDoubleTapToZoom

* Remove dead utils, move utils used only once
2023-10-05 15:28:56 -07:00
Paul Frazee eb7306b165 Profile UI tweaks (#1607)
* Use inverted color instead of primary color for press state

* Fix: add missing border to empty state
2023-10-04 13:28:07 -07:00
Patroll 9278822088 Fix invite codes flash on desktop, use loading placeholder (#1591)
* Fix invite codes flashing untrue value before loaded

* Add loading placeholder for right nav invites
2023-10-04 10:31:43 -07:00
Paul Frazee 2ba0c6a711 Update to react-native@0.72.5 (#1599)
* Update to react-native@0.72.5

* Fix the patch
2023-10-04 10:17:13 -07:00
Baasbase c6da98c818 Fixed a typo on the onboarding recommended screen (#1604) 2023-10-04 08:58:01 -07:00
Paul Frazee b1a1bae02e Onboarding & feed fixes (#1602)
* Fix: improve the 'end of feed' detection condition

* Fix the feeds link on mobile in the empty state

* Align the following empty state better on web

* Dont autofocus the search input in the search tab

* Fix the error boundary render

* Add 'end of feed' CTA to following feed

* Reduce the default feeds to discover now that we have feed-selection during onboarding

* Fix case where loading spinner fails to stop rendering in bottom of feed

* Fix: dont show loading spinner at footer of feed when refreshing

* Fix: dont fire reminders during onboarding

* Optimize adding feeds and update to mirror the api behaviors more closely

* Use the lock in preferences to avoid clobbering in-flight updates

* Refresh the feed after onboarding to ensure content is visible

* Remove the now-incorrect comment

* Tune copy
2023-10-04 08:57:23 -07:00
Bryan Lee 2ca673d7f5 Replace isDesktop with isWeb in DesktopWebTextLink component
- Replaced `useWebMediaQueries().isDesktop` with `isWeb` in `DesktopWebTextLink` component to fix inconsistent behavior due to media query changes.
- `isWeb` doesn't rely on media queries, offering stable judgement and avoiding fluctuation when adjusting the browser window.
2023-10-04 21:47:19 +08:00
Paul Frazee a76fb78d53 Improve time to content in the search page (#1603)
* Improve time to content in the search page

* Improve loading state clarity
2023-10-03 20:01:09 -07:00
Paul Frazee 847d4d19b5 Fix a potential reference error in bottombarweb (#1600) 2023-10-03 19:55:44 -07:00
Paul Frazee 6598fca79d Fix: only use scroll-positioning control on thread when looking at replies (#1587) 2023-10-03 19:55:28 -07:00
Paul Frazee 588659380a Add an illustration to the email verification reminder (#1586)
* Add an illustration to the email verification reminder

* Set height on svg to avoid overflow issues in android
2023-10-03 19:51:57 -07:00
Paul Frazee 001b29a7e3 Fix bad reference when image picker is canceled (#1598) 2023-10-03 17:19:21 -07:00
Paul Frazee fd5bbb2769 Warn the user on links that dont match their text (#1573)
* Add link warning modal when URLs do not match their text

* Simplify the misleading link case for clarity

* Fix typecheck

* fix dark mode

* Give a stronger visual indication of the root domain in the link warning

* More rigorous URL mismatch logic

* Remove debug

---------

Co-authored-by: Ansh Nanda <anshnanda10@gmail.com>
2023-10-02 14:47:39 -07:00
Ansh 2f157c152a fix styling when on FeedsTab (#1574) 2023-10-02 13:28:18 -07:00
Paul Frazee 90b18cf004 Fix alt text selectability in lightbox on web (close #1548) (#1571) 2023-10-02 13:25:41 -07:00
Paul Frazee 64410efafe Small sizing tweaks to the web modal (#1568) 2023-10-02 09:28:04 -07:00
Manish 72e3763446 Add the build step for atproto (#1579)
Co-authored-by: Manish <manish.nayal@nium.com>
2023-10-02 09:18:34 -07:00
Ansh 04fda0f142 Fix 1405 handle android back button in composer (#1446)
* handle android back button in composer

* improve backHandler error handling

* simplify composer onClose functionality
2023-09-29 07:49:59 +04:00
Paul Frazee 1f60e1a748 Fix warning 2023-09-28 20:45:11 -07:00
Bryan Lee 2aae37d67b Improve Device Detection For Better Responsiveness (#1512)
* Refactor `useOnMainScroll` function to use responsive device detection

- Replace static `isDesktopWeb` with `useWebMediaQueries` hook to enable dynamic device type detection.
- Create `useDeviceLimits` hook to dynamically determine `DY_LIMIT_UP` and `DY_LIMIT_DOWN` based on device type.
- Update dependency arrays for the `useCallback` hooks to include new dynamic variables.

* Refactor styles to be responsive to device type

- Create `useStyles` hook that generates styles object based on device type detected from `useWebMediaQueries`.
- Replace static styles object with dynamic styles object generated from `useStyles` hook in components.
- This allows `paddingLeft` values for 'ul' and 'ol' styles to adapt to device type dynamically.
- This allows `maxWidth` values for 'metaItem'' styles to adapt to device type dynamically.

* Remove `isDesktopWeb` in favor of `useWebMediaQueries().isDesktop`

* Refactor `SplashScreen` component for responsive design

* Revision based on review results

* Fix isNative check

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-09-28 12:47:34 -07:00
Paul Frazee 2e5f73ff61 Account quick switch modal (#1567)
* quick switch menu

* Some small tweaks and fixes to the account switch modal

* Factor out the account switcher logic to a hook

* Add haptic feedback on account switcher open

* Fix bad merge

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2023-09-28 12:41:44 -07:00
Gabriel Donadel Dall'Agnol 3e340b336e Add example google-services file (#1545)
* Add example google-services file

* Update build.md
2023-09-28 12:22:23 -07:00
Paul Frazee 65b83e0d6b Merge branch 'Tanza3D-small-pfp-desktop-2' into main 2023-09-28 12:12:03 -07:00
Paul Frazee f5a78c2c86 Slightly larger desktop leftnav pfp 2023-09-28 12:11:09 -07:00
Paul Frazee d4a234d5ce Merge branch 'small-pfp-desktop-2' of https://github.com/Tanza3D/social-app into Tanza3D-small-pfp-desktop-2 2023-09-28 12:08:39 -07:00
Paul Frazee cd3b0e54fb Email verification and change flows (#1560)
* fix 'Reposted by' text overflow

* Add email verification flow

* Implement change email flow

* Add verify email reminder on load

* Bump @atproto/api@0.6.20

* Trim the inputs

* Accessibility fixes

* Fix typo

* Fix: include the day in the sharding check

* Update auto behaviors

* Update yarn.lock

* Temporary error message

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2023-09-28 12:08:00 -07:00
Paul Frazee 16763d1d41 Handle long feed listings on the right nav with scrolling (#1562) 2023-09-28 10:33:14 -07:00
Paul Frazee cc1c9ab305 Merge branch 'mozzius-expand-author-pressable' into main 2023-09-28 10:32:55 -07:00
Paul Frazee d57c8f7bca Fix pointer 2023-09-28 10:32:34 -07:00
Paul Frazee 1aa1b69a94 Merge branch 'expand-author-pressable' of https://github.com/mozzius/bluesky-official-app into mozzius-expand-author-pressable 2023-09-28 10:27:24 -07:00
Paul Frazee e181bae9a6 Merge branch 'left-padding-on-sidebar' of https://github.com/Tanza3D/social-app into main 2023-09-28 10:25:38 -07:00
Tanza 83bb5c1710 Move sidebar by 10px for more even padding (#1543) 2023-09-28 10:24:46 -07:00
Samuel Newman a427010629 Don't render a pressable if there's only one notification author 2023-09-28 11:03:52 +01:00
Eric Bailey b030d94a64 fix link highlighting with mention present (#1544) 2023-09-27 09:15:50 -07:00
Eric Bailey 6d4ad59416 Merge pull request #1553 from mozzius/refreshed-web-repost-dropdown
Use refreshed dropdown style for repost/quote post dropdown
2023-09-27 11:14:56 -05:00
Paul Frazee d87c232660 Improve image cropping on android and introduce aspect ratio field (#1525)
* Fix image cropping on android

* Store and use aspect ratio field in post images (close #1392)
2023-09-27 09:08:21 -07:00
Eric Bailey 6325eff938 Improvements to auto-mentioning users from their profiles (#1556)
* Don't automatically mention users with invalid handles

* don't mention when using did urls

* resolve profile from cache

* a little clearer

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2023-09-27 09:06:33 -07:00
Samuel Newman 5d59ab2ac0 replace dropdown component 2023-09-27 11:32:42 +01:00
Tanza3D e941da646d Move sidebar by 10px for more even padding 2023-09-26 22:12:22 +01:00
Tanza3D 1185ac4053 Small PFP on desktop 2023-09-26 22:07:56 +01:00
Eric Bailey 42723dfaf6 fix 'Reposted by' text overflow (#1535) 2023-09-26 12:24:05 -07:00
dan 95736a5168 Add instructions for running on device (#1541) 2023-09-26 11:46:46 -07:00
Foysal Ahamed b0873cae97 💄 Remove report option on my own post (#1537) 2023-09-26 10:48:07 -07:00
Paul Frazee 83b3b5e1c0 Merge branch 'main' of github.com:bluesky-social/social-app into main 2023-09-25 13:50:44 -07:00
Sasha Mansurov 5f476851a3 Fix gallery image size (#1474)
Fixes #1323
2023-09-25 13:50:29 -07:00
Paul Frazee be2f319a34 Bump android version code 2023-09-22 18:13:07 -07:00
Paul Frazee e9f30894b1 Bump ios build number 2023-09-22 18:08:50 -07:00
Paul Frazee 150fe421d6 Fixes to tests (#1518) 2023-09-22 17:49:41 -07:00
Paul Frazee 41b53ca37b Trim whitespace on invite code (#1517) 2023-09-22 17:04:58 -07:00
Paul Frazee b7697f08d6 Accessibility fixes: add missing labels, dynamically scale home header (#1516)
* Improve labels

* Dynanically adjust home header to account for font scaling
2023-09-22 17:04:47 -07:00
Paul Frazee 88b95df200 Update backend routes to match frontend (#1515) 2023-09-22 17:04:36 -07:00
Eric Bailey 1b3b4c6692 Merge pull request #1513 from bluesky-social/eric/app-855-update-language-settings-in-app
update translator link show logic
2023-09-22 10:22:28 -05:00
Eric Bailey 28f5a8e44d bool 2023-09-22 09:46:46 -05:00
Eric Bailey 4fa576cb30 update translator link show logic 2023-09-22 09:24:48 -05:00
Paul Frazee 146132a080 Bump ios buildnumber 2023-09-21 21:01:15 -07:00
Paul Frazee 8584009bae Move home feed and thread preferences to server (#1507)
* Move home feed and thread preferences to server

* Fix thread usage of prefs

* Remove log

* Bump @atproto/api@0.6.16

* Improve type usage
2023-09-21 21:00:32 -07:00
Paul Frazee 28b692a118 Switch the date input web version to the date-input web control (#1510)
* Switch the date input web version to the date-input web control

* Fix types
2023-09-21 17:41:09 -07:00
Paul Frazee 1abe5270dd Wrap the post thread screen in an observer (#1509) 2023-09-21 17:36:50 -07:00
Paul Frazee 44985d2312 Dont reposition load latest button on desktop (#1508) 2023-09-21 17:36:33 -07:00
Paul Frazee 8aabbb9b6f Minor alignment fix 2023-09-21 15:07:56 -07:00
Paul Frazee 7931787dd7 Tune the treeview mode to use larger immediate-children (#1506)
* Tune the treeview mode to use larger immediate-children

* Fix display name alignment
2023-09-21 13:47:42 -07:00
Eric Bailey 8a5f9cd43d Language settings updates, new primary language setting (#1471)
* move content languages to screen

* add dropdown library, style primary lang select

* update settings button

* show selected langauges in button

* use primary language in translator link

* update copy

* lint
2023-09-21 11:33:19 -07:00
Paul Frazee 335061f763 Bump ios build number 2023-09-20 19:48:40 -07:00
Eric Bailey e837a499f3 add suggested follows metrics (#1503) 2023-09-20 19:48:03 -07:00
Paul Frazee 5a945c2024 Prefilter the mergefeed to ensure a better mix of following and custom feeds (#1498)
* Prefilter the mergefeed to ensure a better mix of following and custom feeds

* Test suite improvements & tests for the mergefeed (#1499)

* Disable invite codes test for now

* Update test sim to latest iphone

* Introduce TestCtrls driver

* Add mergefeed tests
2023-09-20 19:47:56 -07:00
Paul Frazee 68dd3210d1 Improve consistency of floating button positioning (#1501) 2023-09-20 19:45:05 -07:00
Bryan Lee f5e9e4af89 Updates InviteCodes modal to use dynamic/responsive styles on web (#1502) 2023-09-20 19:16:33 -07:00
Eric Bailey 6df1bcad31 add suggested follow section to profile header (#1481)
* add suggested follow section to profile header

* fix button overflow

* don't even render on preview

* fix useFollowDid and FollowButton race condition

* add section header, close button, active state

* lighten icon
2023-09-20 19:16:11 -07:00
Paul Frazee 498c3e2c27 Tune the treeview UI a bit more (#1494) 2023-09-20 19:13:54 -07:00
Paul Frazee acc98ac1ab Dont show the reply prompt if minimal shell mode triggers (#1495) 2023-09-20 19:12:40 -07:00
Paul Frazee 95e8a6bf41 Even less intrusive status line for posts (#1496) 2023-09-20 19:11:28 -07:00
Paul Frazee 8f8bed9a1c Fix a mobx complaint about mutations (#1500) 2023-09-20 19:11:07 -07:00
dan d33aaf57ad Add a tip about Android emulator memory limit (#1492) 2023-09-20 12:03:07 -07:00
Eric Bailey a1c040cd79 Merge pull request #1476 from bluesky-social/bnewbold/bump-api-dep
yarn: bump @atproto/api to remove redundant deps
2023-09-20 11:07:13 -05:00
Eric Bailey 5665968f72 Merge remote-tracking branch 'origin' into bnewbold/bump-api-dep
* origin:
  Allow touch at the top of the lightbox (#1489)
  Bump ios build number
  Feeds tab fixes (#1486)
  Nicer 'post processing status' in the composer (#1472)
  Inline createPanResponder (#1483)
  Tree view threads experiment (#1480)
  Make "double tap to zoom" precise across platforms (#1482)
  Onboarding recommended follows (#1457)
  Add thread sort settings (#1475)
2023-09-20 11:03:57 -05:00
dan cd96f8dcc8 Allow touch at the top of the lightbox (#1489) 2023-09-20 12:41:18 +01:00
Paul Frazee 6db7113108 Bump ios build number 2023-09-19 21:45:03 -07:00
Paul Frazee 971c8025e6 Feeds tab fixes (#1486)
* Bold the saved feeds on mobile

* Improve the saved feeds loading state

* Add soft reset handler to feeds page

* Show feed descriptions in profile listing

* Add an 'about this feed' modal

* Fix type assertion
2023-09-19 21:44:23 -07:00
Paul Frazee 753fb8bfbc Nicer 'post processing status' in the composer (#1472) 2023-09-19 20:40:58 -07:00
dan 8b8fba7284 Inline createPanResponder (#1483) 2023-09-20 03:46:59 +01:00
Paul Frazee 1af8e83d53 Tree view threads experiment (#1480)
* Add tree-view experiment to threads

* Fix typo

* Remove extra minimalshellmode call

* Fix to parent line rendering

* Fix extra border

* Some ui cleanup
2023-09-19 19:08:11 -07:00
dan d2c253a284 Make "double tap to zoom" precise across platforms (#1482)
* Implement double tap for Android

* Match the new behavior on iOS
2023-09-19 18:32:44 -07:00
Ansh 859588c3f6 Onboarding recommended follows (#1457)
* upgrade api package

* add RecommendedFollows as a step in onboarding

* add list of recommended follows from suggested actor model

* remove dead code

* hoist suggestedActors into onboarding model

* add comments

* load more suggested follows on follow

* styling changes

* add animation

* tweak animations

* adjust styling slightly

* adjust styles on mobile

* styling improvements for web

* fix text alignment in RecommendedFollows

* dedupe inserted suggestions

* fix animation duration

* Minor spacing tweak

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com> and Eric Bailey <git@esb.lol>
2023-09-20 01:18:50 +05:30
Paul Frazee da8499c881 Add thread sort settings (#1475)
* Add thread sorting preferences

* UI tweaks

* Tweak settings

* Tune the copy
2023-09-19 12:24:58 -07:00
bryan newbold 63527493fd yarn: bump @atproto/api to remove redundant deps 2023-09-18 18:39:26 -07:00
Paul Frazee 9c4374f66a 1.51 2023-09-18 14:34:41 -07:00
Foysal Ahamed 3c4899b3c4 🐛 Handle middle mouse click on feed list items (#1469)
* 🐛 Handle middle mouse click on feed list items

* ♻️ Refactor the event listener and turn it into a dedicated hook for web

* 🧹 Cleanup unnecessary Link changes

* Fix import

* Create native version of useAuxClick

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-09-18 14:34:12 -07:00
Paul Frazee 255beb0c1f Various release fixes (#1470)
* Fix error state for recommended feeds

* Handle failure to set default feeds more safely

* Dont return undefined within react-query

* Update yarn.lock
2023-09-18 13:38:33 -07:00
Paul Frazee 157daad605 Update yarn.lock 2023-09-18 12:05:18 -07:00
Paul Frazee ea885339cf Feed UI update working branch [WIP] (#1420)
* Feeds navigation on right side of desktop (#1403)

* Remove home feed header on desktop

* Add feeds to right sidebar

* Add simple non-moving header to desktop

* Improve loading state of custom feed header

* Remove log

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

* Remove dead comment

---------

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

* Redesign feeds tab (#1439)

* consolidate saved feeds and discover into one screen

* Add hoverStyle behavior to <Link>

* More UI work on SavedFeeds

* Replace satellite icon with a hashtag

* Tune My Feeds mobile ui

* Handle no results in my feeds

* Remove old DiscoverFeeds screen

* Remove multifeed

* Remove DiscoverFeeds from router

* Improve loading placeholders

* Small fixes

* Fix types

* Fix overflow issue on firefox

* Add icons prompting to open feeds

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

* Merge feed prototype [WIP] (#1398)

* POC WIP for the mergefeed

* Add feed API wrapper and move mergefeed into it

* Show feed source in mergefeed

* Add lodash.random dep

* Improve mergefeed sampling and reliability

* Tune source ui element

* Improve mergefeed edge condition handling

* Remove in-place update of feeds for performance

* Fix link on native

* Fix bad ref

* Improve variety in mergefeed sampling

* Fix types

* Fix rebase error

* Add missing source field (got dropped in merge)

* Update find more link

* Simplify the right hand feeds nav

* Bring back load latest button on desktop & unify impl

* Add 'From' to source

* Add simple headers to desktop home & notifications

* Fix thread view jumping around horizontally

* Add unread indicators to desktop headers

* Add home feed preference for enabling the mergefeed

* Add a preference for showing replies among followed users only (#1448)

* Add a preference for showing replies among followed users only

* Simplify the reply filter UI

* Fix typo

* Simplified custom feed header

* Add soft reset to custom feed screen

* Drop all the in-post translate links except when expanded (#1455)

* Update mobile feed settings links to match desktop

* Fixes to feeds screen loading states

* Bolder active state of feeds tab on mobile web

* Fix dark mode issue

---------

Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Ansh <anshnanda10@gmail.com>
2023-09-18 11:44:29 -07:00
Eric Bailey 3118e3e933 improve drawer reflow for large text (#1464) 2023-09-15 15:18:09 -07:00
Paul Frazee 754663e5c3 Add findlast shim for outdated browsers (#1463)
* Add findlast shim for crappy browsers

* Fix types
2023-09-15 14:40:15 -07:00
Paul Frazee 0672451ddc Sanitize BIDI characters from display names (#1462) 2023-09-15 14:05:45 -07:00
Paul Frazee 763a50692f Readd hitslop to post controls (#1461)
* Readd hitslop to post controls

* Bigger hitslop on bigger controls
2023-09-15 14:01:44 -07:00
Eric Bailey e3de91788e Merge pull request #1452 from bluesky-social/eric/update-image-picker
bump picker deps
2023-09-15 14:06:55 -05:00
Eric Bailey 8593f41f36 don't thread a self-repost of a self-reply (#1450)
* don't thread a self-repost of a self-reply

* typo
2023-09-15 09:34:12 -07:00
Ansh 188d4893f9 add React Query and hook up to existing functionality (#1358)
* add React Query and hook up to existing functionality

* wire in remote data, add error message

* remove hard-coded feeds

* oops fix logic

* add loading state

* fix loading on mobile

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2023-09-15 09:32:44 -07:00
Eric Bailey 84b7edd9db Merge pull request #1451 from bluesky-social/eric/theme-switching-web
fix theme switching on web without refresh
2023-09-15 11:06:06 -05:00
Eric Bailey 0e8d564555 link out to more context 2023-09-15 11:00:06 -05:00
Eric Bailey 01870883d6 clean up into hook 2023-09-14 18:15:17 -05:00
Ansh 50f811666a Fix Profile link from drawer on Mobile Web (#1437)
* 🗺️ fix an issue where web devices that see the Drawer couldn't access Profile from said Drawer

* fix routes on web

* handle profile button active state

* add hack for web navigation

* fix comment

---------

Co-authored-by: Micah Maligie <kerosuppi@gmail.com>
2023-09-14 16:10:59 -07:00
Eric Bailey e643c43459 add build:apk and apk config (#1454) 2023-09-14 16:06:25 -07:00
bnewbold 37252e90b4 update Bluesky PBLLC to PBC (Public Benefit Corporation) (#1453) 2023-09-14 14:51:06 -07:00
Eric Bailey bd8dac0154 bump picker deps 2023-09-14 15:17:57 -05:00
Eric Bailey 9a86f0dae4 simplify logic 2023-09-14 13:23:20 -05:00
Eric Bailey a88ac217c2 fix theme switching on web without refresh 2023-09-14 12:55:47 -05:00
Ansh b9268d503a wrap impersonation text (#1447) 2023-09-13 11:12:58 -07:00
Ansh 9c72fcc9b4 share mute list link with did not handle (#1438) 2023-09-12 15:35:34 -07:00
Ansh 4977a5d2a3 Fix 651 Make followers and following button into a <Link> (#1429)
* make followers and following button into a Link

* make buttons into `<a>` tags
2023-09-12 13:47:55 -07:00
Paul Frazee 2b6fe4cb50 1.50 2023-09-11 18:23:40 -07:00
Paul Frazee 9b5bd53f84 Remove accessibilityRole=button on dropdown mask, it causes issues on firefox (#1436) 2023-09-11 18:09:02 -07:00
Paul Frazee 9e8b14f710 Add birth date gating to moderation settings (#1435)
* Add birth date preference, modal to set, link in settings, and age gate in moderation

* Styling fixes for android

* Fix types
2023-09-11 18:04:09 -07:00
Paul Frazee 0090371011 Paginate the PostThread to avoid rendering too many posts and crashing the app on large threads (#1432) 2023-09-11 17:41:00 -07:00
Eric Bailey 21371081c6 fix theme flashing (#1417)
* fix theme flashing

* remove memo

* use AppState listener
2023-09-11 14:39:31 -07:00
Paul Frazee f8c611118e Separate list deletion into chunks (close #1430) (#1431) 2023-09-11 14:28:30 -07:00
Paul Frazee 977f9228f8 Revert "Fix 1338 mark notifications read with timeout and on app state change (#1340)" (#1433)
This reverts commit d2e65d2a7f.
2023-09-11 14:28:13 -07:00
Paul Frazee 57c10cbcab Bump @atproto/api@0.6.12 (#1434) 2023-09-11 14:28:02 -07:00
Ansh 27cb1bd276 make sure click events on the emoji picker don't bubble up to the mask (#1427) 2023-09-11 13:31:14 -07:00
Ansh bcd63e710c Lytic analytics migration (#1428)
* add new api event proxy

* add new apiHost for analytics web
2023-09-11 11:41:27 -07:00
Ansh 01775e5bff add new api event proxy (#1400) 2023-09-11 22:36:49 +05:30
uakci 731d84faaa composer: inhibit Cmd-Enter from inputting newline (#1421) 2023-09-11 09:02:14 -07:00
Bossett 775aa87540 Update time.ts to handle very small or negative time differences (#1390)
* Update time.ts to handle very small or negative time differences

Right now, posts can appear to be from the future with a negative time difference (i.e. -3s appears). This change defines 'NOW' as less than 5 seconds old, and returns 'now' in that case.

It's not clear how localisation is handled - this may need translation.

* Add test for 'now' in time/ago(...)

Add tests for ago() for right now (i.e. 'now') and 10s ago to ensure the seconds case is still tested
2023-09-08 08:57:22 -07:00
dan 6d73ed96e1 Make Sentry optional (#1416)
* Move app.json -> app.config.js

* Make Sentry optional
2023-09-08 08:47:01 -07:00
dan adc5b75b2b Fix autocomplete avatar alignment (#1414) 2023-09-08 16:30:42 +01:00
dan 44a00e1b7e Set crop mode to None by default (#1415)
Fixes #1263

Previously, opening the crop tool had 1:1 selected by default. So if you opened it and pressed Done without changing anything, it would format the image as 1:1.

After this change, None is selected by default. So if you open the crop tool and press Done without changing anything, nothing will change.
2023-09-08 16:30:00 +01:00
Micah Maligie c10cb8e972 💅 disallow overflow outside of post content area(s) (#1395) 2023-09-07 18:13:28 -07:00
dan 8a93321fb1 Give explicit names to MobX observer components (#1413)
* Consider observer(...) as components

* Add display names to MobX observers

* Temporarily suppress nested components

* Suppress new false positives for react/prop-types
2023-09-07 17:36:08 -07:00
dan 69209c988f Update build instructions (#1406)
- Use pnpm
- Clarify when prebuild is needed
2023-09-07 17:29:16 -07:00
dan a5b89dffa6 Add ESLint React plugin (#1412)
* Add eslint-plugin-react

* Enable display name rule
2023-09-07 16:38:57 -07:00
Ansh 00595591c4 Hotfix: Fix unable to enter invite code (#1409)
* Remove accessibilityRole from invite code input

* remove unused styles
2023-09-07 13:50:24 -07:00
Paul Frazee 2963fb4c6d Fix scrollbar gutters on web (#1401)
* Fix scrollbar gutters on web

* Fix typecheck
2023-09-07 09:32:14 -07:00
dan 138e6d9b97 Fix lint-staged not allowing edits to Markdown files (#1402)
* Remove unnecessary overrides config

* Only lint-staged code files
2023-09-07 02:06:07 +01:00
dan 82a17db594 Always prebuild cleanly (#1399) 2023-09-06 17:21:48 -07:00
Eric Bailey b76b56b4a4 Merge pull request #1397 from bluesky-social/eric/fix-startup-error
prevent error screen on startup
2023-09-06 16:33:56 -05:00
Eric Bailey fdc4ae1f7b prevent error screen on startup 2023-09-06 16:27:44 -05:00
Minseo Lee b6d239aa37 Update ProfileHeader.tsx (#1389) 2023-09-06 13:45:50 -07:00
Ansh 63f0b2f0e5 fix text color on waitlist join (#1387) 2023-09-06 09:37:07 -07:00
dan dd5697329f Remove unnecessary deps from package.json (#1393) 2023-09-06 09:36:45 -07:00
dan 19adb10de1 Use frozen lockfile for CI workflows (#1394) 2023-09-06 09:26:19 -07:00
Paul Frazee 565c1a0510 1.49 2023-09-05 14:04:36 -07:00
Paul Frazee 52f3810e81 Update tests 2023-09-05 14:03:39 -07:00
Paul Frazee 411c1581f6 Fix thread positioning on load (#1385)
* Fix minIndex for consistent positioning on thread view

* Enable maintainVisibleContentPosition on android now that it's supported
2023-09-05 13:40:38 -07:00
Eric Bailey e3e91816d1 Upgrade to Expo 49 (#1331)
* update to expo 49

* update expo-camera to fix console erorrs

* run doctor again

* fix ts errors

* patch @sentry/react-native

Getting `cannot read property 'ignoreLogs' of undefined` in this file.
Ironically, this may be a cyclical imports problem. LogBox isn't enabled
in production, so this patch should only affect dev/test.

* fix type error

* reinstall newer reanimated

* pin expo-dev-client to fix reanimated dev-build issue

* fix type errors, fix bad conflict res

* Fix to notifications badge z-index on desktop

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-09-05 12:23:22 -07:00
dan 9a3fa512eb Enable Fast Refresh for web (#1383) 2023-09-05 11:13:42 -07:00
Paul Frazee 764c7cd569 Updates to use dynamic/responsive styles on web (#1351)
* Move most responsive queries to the hook

* Fix invalid CSS value

* Fixes to tablet render of post thread

* Fix overflow issues on web

* Fix search header on tablet

* Fix QP margin in web composer

* Fix: only apply double gutter once to flatlist (close #1368)

* Fix styles on discover feeds header

* Fix double discover links in multifeed
2023-09-05 10:42:19 -07:00
dan be8084ae10 Fix mismatching button label for speech recognition (#1372)
* Fix mismatching a11y label

* Update all new post button labels to match
2023-09-05 16:55:39 +01:00
dan 161746519c Fix caret jumps for web composer (#1374)
* Fix caret jumps for web composer

* Autofocus at the end on mount
2023-09-05 08:21:10 +01:00
Paul Frazee 419ac2d0df Improved mute alerting (#1355)
* Use a simpler mute alert when viewing a post by a muted user

* Dont mute self-QPs when we're overriding a mute

* Fix types
2023-09-01 12:30:02 -07:00
Paul Frazee 3e96373903 Sort thread replies by likes (#1356)
* Sort replies by likes

* Types fix
2023-09-01 11:54:51 -07:00
Ansh 4cd3ddecad refresh when empty (#1354) 2023-08-31 16:45:18 -07:00
Samuel Newman f16cc9537a show images in notification if post also has record embed (#1349) 2023-08-31 16:44:42 -07:00
Eric Bailey ba393cd642 encode all unsafe characters in post text for translate URL (#1345) 2023-08-31 16:39:18 -07:00
Ansh d2e65d2a7f Fix 1338 mark notifications read with timeout and on app state change (#1340)
* mark notifications read with timeout and on app state change

* fix type
2023-08-30 18:06:02 -07:00
Ansh 4ac82536c9 Fix 1304 refresh notifications when a notif is clicked or received (#1339)
* refresh notifications when a notif is clicked

* make notification syncing smarter

* allow enabled appview proxy on mobile

* put back syncqueue code
2023-08-30 18:04:33 -07:00
Ansh 4bec7c1d85 Fix #1328 fix emoji picker scroll on firefox and chrome (#1332)
* fix emoji picker scroll on firefox and chrome

* click outside emoji picker to dismiss

* dynamically position emoji picker

* dynamically placing picker
2023-08-30 17:58:24 -07:00
Paul Frazee 04992f14f1 Improvements to UI in web logged-out views (#1341)
* Add LoggedOutLayout for desktop/tablet web

* Avoid screen flash in the transition to onboarding

* Fix comment
2023-08-30 17:55:01 -07:00
Ansh a498acab6e fix types for breakpoints on mobile (#1342) 2023-08-30 17:02:23 -07:00
Paul Frazee f9cab178b9 Merge branch 'ansh/app-812-add-custom-feed-discovery-to-onboarding' into main 2023-08-30 16:18:21 -07:00
Paul Frazee 59dcedeea2 Fix: remove bottom bar during onboard 2023-08-30 16:15:06 -07:00
Paul Frazee 8bc8dcc094 Simplify the RecommendedFeeds with breakpoint components 2023-08-30 16:13:09 -07:00
Paul Frazee 3fa9b6daba Fix types and remove dead code 2023-08-30 15:30:26 -07:00
Ansh 9446c67880 Move PreferencesHomeFeed to a screen instead of a modal (#1335)
* move `PreferencesHomeFeed` to a screen instead of a modal

* add web route for home feed preferences

* upgrade `@miblanchard/react-native-slider` to fix lint

* fix web route naming

* fix desktop web styling

* add `react-native-slider` mock
2023-08-30 15:21:12 -07:00
Paul Frazee a29f10aefe Moderation settings fixes (#1336)
* Default isAdultContentEnabled to false on all devices.

The original intent of setting the default based on the device was
to make the maximally-permissive choice. It turns out this was a
mistake as it created sync issues between devices; users would be
confused by the lack of congruity between them. We have to go with
false by default to ensure sync is retained.

* Update preferences model to use new sdk api

* Delete dead code

* Dont show the iOS adult content warning in content filtering settings if adult content is enabled

* Bump @atproto/api@0.6.8

* Codebase style consistency
2023-08-30 15:19:19 -07:00
Paul Frazee 8ed6e72ea4 More fixes to mobile onboard 2023-08-30 15:15:10 -07:00
Paul Frazee 05d1d8d8a4 Fix onboarding on mobile web 2023-08-30 14:57:03 -07:00
Paul Frazee cd8ae1298e Tweak dark mode on web 2023-08-30 10:56:07 -07:00
Paul Frazee b847917969 Tweaks to mobile onboard 2023-08-29 23:02:37 -07:00
Paul Frazee 5e765bf1cb Rework web onboarding 2023-08-29 22:56:13 -07:00
Paul Frazee 5d9534ca72 Move onboarding to the withAuthRequired HOC 2023-08-29 20:20:51 -07:00
Eric Bailey 3a90b479fd fix lightbox on search page (#1330)
* fix lightbox on search page

* add styles to mobile web too
2023-08-29 17:41:50 -07:00
Eric Bailey 22b7964b5e Merge pull request #1334 from bluesky-social/eric/fix-android-perms-noop
handle stuck state with granular android permissions
2023-08-29 19:27:43 -05:00
Eric Bailey 5710140a73 handle stuck state with granular android permissions 2023-08-29 17:09:38 -05:00
Ansh Nanda bf37913701 fix onboarding on web 2023-08-29 12:16:26 -07:00
Eric Bailey 6bee7d518b bump yarn (#1329) 2023-08-29 11:01:14 -07:00
bnewbold 461c1da6de update golang to v1.21; set GOEXPERIMENT=loopvar (#1256)
* golang: update to v1.21, and set GOEXPERIMENT=loopvar

* golang: go mod tidy
2023-08-28 19:35:23 -07:00
Paul Frazee 40e030793a 1.48 2023-08-28 19:11:15 -07:00
Paul Frazee 2619c55c10 Test fixes 2023-08-28 19:10:36 -07:00
Ansh Nanda 742440c22d add more tracking 2023-08-28 18:55:40 -07:00
Ansh Nanda 8f0f3be8fb start onboarding only for new account creations 2023-08-28 18:53:58 -07:00
Ansh Nanda bcef7400db simplify onboarding model function naming 2023-08-28 18:47:56 -07:00
Eric Bailey 1c460c40f4 Make posts behave more like links (#1316)
* use cursor for post cards

* ignore type error

* handle meta keys on non native links

(cherry picked from commit daccafea0b7ab21af6572767e496d20f32ead353)

* remove cursor on non-post notifications, not quite right

* Simplify link handling

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-08-28 18:41:28 -07:00
Paul Frazee cc2838761b Replace web editor link behavior (#1319)
* Replace web editor link behavior (close #1293) (close #1292)

* Update link decorator to match rich text link detector
2023-08-28 18:40:45 -07:00
Patroll 2c60a0328d Fix: Don't flash default profile avatar and link in nav (#1287) 2023-08-28 17:58:14 -07:00
Paul Frazee 5ee754e6f9 Improvements to feed assembly to avoid possible state issues (#1318)
* Avoid potential dropped posts due to pruning when checking for latest

* Add a sanity check to ensure dup react keys never occur (close #1315)
2023-08-28 17:54:59 -07:00
Ansh Nanda a231fdf64e track onboarding steps 2023-08-28 17:51:26 -07:00
Ansh Nanda b38629b9b5 Merge branch 'main' into ansh/app-812-add-custom-feed-discovery-to-onboarding 2023-08-28 17:49:14 -07:00
Paul Frazee e2f0770b88 Remove ability for Post component to load from URI (close #1302) (#1317) 2023-08-28 17:08:21 -07:00
Ansh Nanda b2dd4ea0f5 fix onboarding stuck on welcome 2023-08-28 15:50:51 -07:00
Ansh Nanda 3f1b313fa4 add custom feed discovery to onboarding 2023-08-28 15:41:02 -07:00
Ansh Nanda c4cf288296 remove logs 2023-08-28 13:39:24 -07:00
Ansh Nanda edfd326069 move onboarding to screens 2023-08-28 13:37:44 -07:00
Ansh Nanda 84e065667a update packages 2023-08-28 13:36:34 -07:00
Eric Bailey c77fd588e9 wrap mutation in runInAction (#1305) 2023-08-28 11:55:56 -07:00
Ansh Nanda 0ca28ccca6 Merge branch 'main' into ansh/app-812-add-custom-feed-discovery-to-onboarding 2023-08-25 14:53:03 -07:00
Ansh Nanda 08fe7fb084 create onboarding model 2023-08-25 14:36:10 -07:00
Paul Frazee a3cb12f584 Move the web composer to the top of the screen (#1282) 2023-08-25 10:30:15 -07:00
Eric Bailey b15a435069 Eslint updates (#1281)
* eslint: Update eslintrc

* eslint: Strings must use singlequote  quotes

* eslint: expected { after if-condition

* eslint: update warnings

* a little cleanup

* remove conflicted file

---------

Co-authored-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-08-25 08:48:38 -07:00
Eric Bailey a0dca81a74 don't mention own user from composer (#1279) 2023-08-25 08:47:47 -07:00
Eric Bailey 953ae9c096 Update/fix ChangeHandle form (#1280)
* fix well-known did copy value

* s/Domain/Host in change handle form
2023-08-25 08:47:12 -07:00
Paul Frazee 37a70f0a67 1.47 build 3 2023-08-24 16:29:44 -07:00
Eric Bailey 462022741d Use Pressable for most links/embeds (#1181)
* delay press on all links

* use Pressable for all accessible links
2023-08-24 16:28:26 -07:00
Eric Bailey 4654a9a45e defer loading of feeds until visible (#1271)
* defer loading of feeds until visible

* Fix: use existing hasLoaded

* Fix: dont query for latest during initial load

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-08-24 16:26:29 -07:00
Paul Frazee a5981e127f Fixes to the feed discovery page (#1238)
* Bump yarn.lock for... reasons

* Restyle DiscoverFeeds empty state (close #1236)

* Fix: maintain search state in feed discovery (close #1204)

* tsconfig fixes

* Tweaks to yarn.lock
2023-08-24 16:16:14 -07:00
Ansh 7410bcd888 setup isAuthenticatedUser in profileUiModel inside an action (#1272) 2023-08-24 16:13:42 -07:00
Paul Frazee c6f321862b Add analytics event for when the app is opened from a notification (#1270)
* Add analytics event for when the app is opened from a notification

* Tweak notification ID
2023-08-24 16:07:16 -07:00
bnewbold a115c17a09 bskyweb: switch from PDS backend to AppView backend (#1065)
* bskyweb: replace PDS with AppView (no auth)

* bskyweb: update example .env files for appview
2023-08-24 16:00:45 -07:00
Paul Frazee 766521486c Increment build number 2023-08-23 17:14:30 -07:00
Paul Frazee c1c02f480b A couple quick notifications fixes (#1262)
* Create separate .easignore to ensure google-services.json goes to eas

* Set the notifications serviceDid according to the session host

* Fix types
2023-08-23 17:10:00 -07:00
Paul Frazee 93d5ba1820 Fix eas.json 2023-08-23 16:35:01 -07:00
Paul Frazee 9bd9d40306 1.47 2023-08-23 16:30:33 -07:00
Ansh 6487a87542 [APP-836] Emoji picker for web (#1254)
* add emoji-mart package for emoji dropdown picker

* remove emoji picker modal

* load emoji mart data not as part of the main bundle

* remove @emoji-mart/data

* setup emoji insertion with events

* get emoji data from local static assets

* close emoji picker after one emoji has been inserted

* Switch emoji picker trigger to an icon

* Update emoji-mart-data.js

* make grabbing emoji data work on more browsers

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-08-23 16:29:23 -07:00
Ansh 8ab5eb6583 [APP-786] Native notifications (#1095)
* move `notifee.ts` to notifications folder

* install expo notifications

* add UIBackgroundMode `remote-notifications` to app.json

* fix notifee import in Debug.tsx

* add `google-services.json`

* add `development-device` class to eas.json

* Add `notifications.ts` for native notification handling

* send push token to server

* update `@atproto/api`

* fix putting notif token to server

* fix how push token is uploaded

* fix lint

* enable debug appview proxy header on all platforms

* setup `notifications.ts` to work with app view notifs

* clean up notification handler

* add comments

* update packages to correct versions

* remove notifee

* clean up code a lil

* rename push token endpoint

* remove unnecessary comments

* fix comments

* Remove old background scheduler

* Fixes to push notifications API use

* Bump @atproto/api@0.6.6

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-08-23 16:28:51 -07:00
Ansh 32b9648931 Fixes #1252 fix alt text setting description toggle (#1261)
* make toggle border width slightly larger

* change copy
2023-08-23 16:16:55 -07:00
Eric Bailey b6317d4ce7 Improve handling of unselecting languanges in composer language menu (#1093)
* allow toggling off/on multiple from main composer lang menu

* fix dropdown styles for long labels

* udpate model to use new string field

* update language UI

* save langs to history on submit

* remove edit

* clean up use new fields

* default to deviceLocales

* fix default valu

* feedback

* use radio icon
2023-08-23 13:40:15 -07:00
Eric Bailey acad8cb455 Merge pull request #1260 from bluesky-social/eric/fix-window
fix bad reference on native
2023-08-23 14:34:41 -05:00
Eric Bailey 473a9fc73e fix bad reference on native 2023-08-23 14:31:08 -05:00
Jake Gold b4931fa185 bskyweb: cache /static/{images,js} for 1 week, other files for 1 hour 2023-08-23 09:23:09 -07:00
Paul Frazee 8e1d0b0396 Change Handle modal DNS instruction tweaks for clarity (#1245)
* Remove potentially confusing period from subdomain in changehandle (close #1004)

* Clarify dns instructions further (close #1087)
2023-08-22 16:37:04 -07:00
Paul Frazee 54706a0437 Fix: update posts rendered in notifs on refresh (close #1196) (#1243) 2023-08-22 16:36:49 -07:00
Paul Frazee fcea27ea0b Fix: include link-meta abort signal for timeout (close #1214) (#1240) 2023-08-22 16:36:36 -07:00
Paul Frazee 54118fe478 Fix feed empty state (close #1235) (#1239) 2023-08-22 16:36:23 -07:00
Eric Bailey 2b13dc0f3a Add likes tab (#1227)
* add likes tab

(cherry picked from commit 6c5ffd964ca0b185ddfc37088d82712a006a1163)

* only show Likes tab to authenticated user

(cherry picked from commit ecc1254411d760158b6d7a4c5f05d940db872dfc)

* Bump @atproto/api@0.6.5

* fix types

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-08-22 12:30:51 -07:00
Eric Bailey 548ec6c82d add title attr to text text links (#1228)
* add title attr to text text links

* Revert "add title attr to text text links"

This reverts commit c028cd184e.

* use css tooltips

* add to expanded post state

* handle theming

* add to bskyweb
2023-08-22 11:04:17 -07:00
Ansh 16b265a861 [APP-834] Allow @ing someone in post directly from profile (#1241)
* setup `initMention` for mobile

* setup creating post with profile tagged on web
2023-08-22 11:01:00 -07:00
Ansh 3aadc43c89 [APP-835] trim end of email during login (#1242)
* trim end of email during login

* Switch trimEnd() to trim()

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-08-22 10:52:59 -07:00
Paul Frazee fb1199e21d Merge branch 'main' of github.com:bluesky-social/social-app into main 2023-08-22 10:50:45 -07:00
Paul Frazee 0a50df1cce Minor formatting 2023-08-22 10:50:19 -07:00
Paul Frazee 17fcc2f25c Merge branch 'fix-empty-search' of https://github.com/darnfish/social-app into darnfish-fix-empty-search 2023-08-22 10:49:53 -07:00
Eric Bailey 13c11801b9 fix thread double border (#1251) 2023-08-22 10:49:03 -07:00
Eric Bailey 09a445d804 do not thread author media filtered feed (#1253) 2023-08-22 10:46:49 -07:00
William Gibson d3f525ab28 Don't submit search if query is empty 2023-08-22 04:27:46 -04:00
Ansh 88357d5c82 add profiles sampling to sentry (#1237) 2023-08-21 16:49:59 -07:00
Eric Bailey 4cc2695d84 allow handles to start with a number (#1231)
* allow handles to start with a number

* don't allow leading hyphen
2023-08-21 13:37:57 -07:00
Paul Frazee 257c15ec1f ios buildNumber 2 2023-08-17 18:59:55 -07:00
Paul Frazee 2198aab8b2 Fix to post meta overflow in highlighted post (#1211)
* Fix to post meta overflow in highlighted post

* Give more space to desktop web
2023-08-17 18:59:25 -07:00
Paul Frazee ee9cf7402f Bump react-native-gesture-handler@2.12.1 2023-08-17 18:30:08 -07:00
Paul Frazee 4342d0f84c 1.46 2023-08-17 18:13:32 -07:00
Paul Frazee 26f4e313ad Bump detox 2023-08-17 18:09:18 -07:00
Paul Frazee c7cad72368 Fix android postmeta alignment 2023-08-17 17:48:45 -07:00
Ansh c219fbe57a upgrade react-native-reanimated (#1198) 2023-08-17 17:47:10 -07:00
Paul Frazee 43d1f002f5 Dont suggest users that are muted or blocked (close #1185) (#1209) 2023-08-17 17:44:48 -07:00
Eric Bailey 55ad56f689 default translate to device language (#1207) 2023-08-17 15:45:36 -07:00
Paul Frazee 4a59178cd2 Update the web composer textinput to an emitter (close #1193) (#1205)
The tiptap useEditor() hook creates an awkward challenge for passing
event handlers into its plugins and native events. By introducing a
memoized editor, we should be able to shuttle events out of tiptap
without retriggering the useEditor hook. The emitter can then change
its registered handlers with each state update.
2023-08-17 13:39:59 -07:00
Paul Frazee 5e63d3164b A set of composer fixes (#1187)
* Don't insert a newline on cmd+entrl (close #1173)

* Don't linkify selected text on url-paste (close #1149)

* Disable the adult content controls if there is no media on the post (close #1169)
2023-08-16 10:46:52 -07:00
Paul Frazee 819340dd3c Shorten links in composer to reduce char usage (#1188)
* Modify toShortUrl() to always include the full domain

* Shorten links in the composer to save on characters

* Apply some limits to the link card suggester
2023-08-16 10:22:50 -07:00
Paul Frazee 5379561934 Tune link card font sizes on desktop 2023-08-16 10:10:39 -07:00
Paul Frazee 79a6819769 Tune up the link card to look nicer, show more info, and layout horizontally on web to user space more nicely (#1190) 2023-08-16 10:02:41 -07:00
Paul Frazee 884e5c9294 Fix to "Load new posts" showing sometimes when there's nothing new to show (#1191)
* Fix to feed item react key value (hopefully)

* Fix false-firing of load more (close #1028)
2023-08-16 10:00:22 -07:00
Jerry Chen b0282865fa Require bluesky-social/social-app for .github/workflows/build-and-push-* (#1152) 2023-08-15 18:50:19 -07:00
Paul Frazee 6ccfb1375a Fix tap target size of post dropdown menus (#1180) 2023-08-15 15:49:52 -07:00
Eric Bailey 0576caae36 use greater of indexedAt or machine clock (#1182)
* use greater of indexedAt or machine clock

* correct mobx usage
2023-08-15 15:18:35 -07:00
Paul Frazee 6964382bad Remove view selector offsetting (close #1174) (#1183)
* Remove view selector offsetting (close #1174)

* Remove horizontal scroll indicator in view selector
2023-08-15 15:13:40 -07:00
Paul Frazee ce1d75e164 Fix link color in dark mode (close #1170) (#1184) 2023-08-15 15:09:54 -07:00
Foysal Ahamed abbc6543f4 Repurpose report post modal and re-use for list reporting (#1070)
*  Repupose report post modal and re-use for list reporting

*  Allow reporting a feed generator

*  ♻️ Refactor report modal into one shared component for reporting different collections

*  Adjust report option selector in tests

*  Add test for list reporting

* ♻️  Refactor reason options and add options for list and feedgen

* 🧹 Cleanup remaining todo

* Fix to mutelist react keys

* Fix regression from rebase

* Improve customfeed mobile header

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-08-15 14:32:06 -07:00
Paul Frazee a5762c2d7d Clear caches on session change (#1179) 2023-08-15 12:55:51 -07:00
Eric Bailey 331c506728 fix prefetching lightbox images (#1163)
* fix prefetching lightbox images

* use array signature

* fix other RN native image usage

* delay prefetching to allow UI to re-render
2023-08-15 12:53:03 -07:00
Ansh 77178844fd [APP-798] add key to ProfileCardPills render method (#1158)
* add key to `ProfileCardPills` render method

* More reliable key generation

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-08-15 11:32:57 -07:00
Paul Frazee 94527a4d9e 1.45 2023-08-15 10:08:07 -07:00
Paul Frazee f4891a47a1 Fix to feeds view in profiles (#1171)
* Fix load of feeds tab

* Add e2e test for profile feeds
2023-08-15 09:59:49 -07:00
Eric Bailey a3af2a2b85 use user.handle response from getProfile, not getSession (#1165) 2023-08-15 09:59:34 -07:00
Ansh 4d953f0504 Fixes #1124: Trim alt text before saving (#1159)
* trim alt text before setting

* Drop `trim` from helpers.ts
2023-08-14 11:41:44 -05:00
Paul Frazee 003a75fd8e Build increment 2023-08-11 08:50:59 -07:00
Paul Frazee 993026cbc4 Fix: close keyboard when viewing the self-label modal (#1153) 2023-08-11 08:50:24 -07:00
Paul Frazee 821b9ba6e6 1.44 2023-08-10 16:54:52 -07:00
Paul Frazee c8bc3a1822 Test fix 2023-08-10 16:54:13 -07:00
Paul Frazee 79d78b5f59 Fix: set background color of the view selector (#1151) 2023-08-10 16:47:34 -07:00
Paul Frazee e5b7a894a3 More useful block-state information in embeds (#1150)
* More useful block-state information in embeds

* Tune copy
2023-08-10 16:25:11 -07:00
Paul Frazee ac6f6eef66 Replace the emojis in the self-labeler with a remove button (#1147) 2023-08-10 12:54:27 -07:00
Eric Bailey cc3fcb1645 Adds profile media tab (#1137)
* add media tab

* fix loading state

* cleanup

* update naming

* upgrade api package

* fix load state

* add scroll view to tabs

* fix overflow on mobile web
2023-08-10 10:50:37 -07:00
Paul Frazee 03d152675e Add self-labeling controls (#1141)
* Add self-label modal

* Use the shield-exclamation icon consistently on post moderation

* Wire up self-labeling

* Bump @atproto/api@0.6.0

* Bump @atproto/dev-env@^0.2.3

* Add e2e test for self-labeling

* Fix types
2023-08-09 17:34:16 -07:00
Eric Bailey 48813a96d6 wait for list memberships to load before becoming interactive (#1140)
* wait for list memberships to load before becoming interactive

* add spinner

* remove secondary spinner
2023-08-09 15:50:40 -07:00
Paul Frazee b5511e1450 Bump @atproto.api@0.5.4 2023-08-08 16:24:19 -07:00
Paul Frazee bbe9861eef Add alerts to embeds (#1138)
* Add alerts to embeds

* Add images to the mock data

* Fix types
2023-08-08 15:54:36 -07:00
Paul Frazee e51dbefd0a Fix to feed discovery (#1134)
* Fixes to debounce

* Less aggressive debounce
2023-08-08 11:56:34 -07:00
Paul Frazee 72e459f3e2 Fix rendering of feed and list post-embeds (close #1131) (#1135) 2023-08-08 11:34:33 -07:00
Paul Frazee 3c29a1be4d Fix to react-key uniqueness in feeds (#1136) 2023-08-08 11:34:19 -07:00
Eric Bailey 872a7f93f4 Merge pull request #1133 from bluesky-social/eric/fix-thread-ui
fix thread spacing and reply lines
2023-08-08 12:32:44 -05:00
Eric Bailey c48ff1855c remove unused styles 2023-08-08 11:36:48 -05:00
Eric Bailey 8cb076f5fd fix thread spacing and reply lines 2023-08-08 11:34:48 -05:00
Eric Bailey f7a0ea3e13 Merge pull request #1132 from bluesky-social/eric/fix-reply-line
fix long thread styling
2023-08-08 10:21:21 -05:00
Eric Bailey 85b4de701b fix long thread styling 2023-08-08 10:08:57 -05:00
Paul Frazee 7bbaccad1c Fix: dont use profile preview modal on mobile web (#1125) 2023-08-07 15:01:26 -07:00
Paul Frazee 89fc975a15 Perf: switch to stable react keys (#1113) 2023-08-05 11:29:55 -07:00
Eric Bailey d53cbb91bb fix post thread feedback regression (#1109) 2023-08-04 13:48:22 -07:00
Paul Frazee f63ed57e3b Fix repeated firing of scroll into view on large threads (#1108) 2023-08-04 13:48:07 -07:00
Paul Frazee 5d2d0976ba Increment ios build number 2023-08-04 12:14:53 -07:00
Paul Frazee d9cf37aecf Collection of small UI fixes & improvements (#1104)
* Fix black bar appearing in link card images

* Include QPs in posts cache

* Fix like color for feed likes in notifications

* Fix post embed spacing
2023-08-04 12:14:21 -07:00
Eric Bailey c42b9c1b4e fix feed UI alignment issues (#1106)
* fix feed alignment issues

* fix reason line alignment
2023-08-04 12:13:41 -07:00
Paul Frazee c4459ec4e8 Simplify the after-post logic to avoid a UI bug (#1105) 2023-08-04 09:26:09 -07:00
Paul Frazee b154d3ea21 Labeling & moderation updates [DRAFT] (#1057)
* First pass moving to the new labeling sdk (it compiles)

* Correct behaviors around interpreting label moderation

* Improve moderation state rendering

* Improve hiders and alerts

* Improve handling of mutes

* Improve profile warnings

* Add profile blurring to profile header

* Add blocks to test cases

* Render labels on profile cards, do not filter

* Filter profiles from suggestions using moderation

* Apply profile blurring to ProfileCard

* Handle blocked and deleted quote posts

* Temporarily translate content filtering settings to new labels

* Fix types

* Tune ContentHider & PostHider click targets

* Put a warning on profilecard label pills

* Fix screenhider learnmore link on mobile

* Enforce no-override on user avatar

* Dont enumerate profile blur-media labels in alerts

* Fixes to muted posts (esp quotes of muted users)

* Fixes to account/profile warnings

* Bump @atproto/api@0.5.0

* Bump @atproto/api@0.5.1

* Fix tests

* 1.43

* Remove log

* Bump @atproto/api@0.5.2
2023-08-03 22:08:30 -07:00
Jake Gold 3ae5a6b631 bskyweb: publish Bluesky IPs 2023-08-03 11:02:34 -07:00
Paul Frazee 96280d5f1a Improve the profile preview with "swipe up to view" and local cache optimization (#1096)
* Update the ProfilePreview to use a swipe-up to navigate

* Use the profile cache to optimize load performance

* Hack to align the header in the profile preview against the screen view

* Fix profiles cache logic to ensure cache is used

* Fix dark mode on profile preview
2023-08-03 10:25:17 -07:00
Eric Bailey 1211c353d0 resolve did before loading feed (#1092)
* resolve did before loading feed

* add loader

* wrap with authRequired, handle errors
2023-08-03 10:00:38 -07:00
Paul Frazee a63f97aef2 Use a post and handle-resolution cache to enable quick postthread loading (#1097)
* Use a post and handle-resolution cache to enable quick postthread loading

* Fix positioning of thread when loaded from cache and give more visual cues

* Include parent posts in cache

* Include notifications in cache
2023-08-03 09:44:43 -07:00
William 7256169506 Change button based on option (#1077) 2023-08-02 09:56:47 -07:00
Ansh f33fdb97b6 change copy in `InviteCodes.tsx (#1082) 2023-08-02 09:34:39 -07:00
Emily Liu 869cc3aaae update readme (#1083)
Co-authored-by: Emily Liu <emilyliu732l@gmail.com>
2023-08-02 09:32:13 -07:00
Paul Frazee 4effa88aec 1.42 2023-07-28 21:14:02 -05:00
Paul Frazee 1195f28992 Fix keyboard support on the dropdown (#1073)
* Fix: dropdown now supports accessibility labels and keyboard controls

* Fix event propagation around the post dropdown
2023-07-28 18:12:21 -05:00
Paul Frazee 45da8a86c9 Fix to tests 2023-07-28 16:32:55 -05:00
Ansh 3b8b562268 [APP-737] Accessible native dropdown menu (#988)
* fix comments

* add zeego package

* get basic native dropdown working

* add separator and icon components

* refined native dropdown component

* add android build properties to app.json

* move `PostDropdownBtn` to its own component

* fix selectors issue

* move `PostDropdownBtn` to its own component

* fix hitslop

* fix post dropdown hitslop

* fix android dropdown icons

* move `UserAvatar.tsx` to native dropdown

* use native dropdown in `ProfileHeader.tsx`

* use native dropdown in `PostThreadItem.tsx`

* use native dropdown in `UserBanner.tsx`

* use native dropdown in `CustomFeed.tsx`

* replace `testId` with `testID` (which is what is used everywhere)

* move `Settings.tsx` to use native dropdown

* create jest mocks for zeego

* create jest mock for `zeego/dropdown-menu`

* web styles for native dropdown

* remove example native dropdown

* adjust web styles

* fix propagation

* fix pressable in `Settings.tsx`

* animate dropdown on web

* add keyboard nav and hover styles

* add hitslop to constants

* add comments to NativeDropdown component

* temporarily removed android icons

* add testID to PostDropdownBtn

* add testID back to all NativeDropdown button implementations

* add postDropdownBtn testID

* add testID to dropdown items

* remove testID from dropdown menu item

* refactor home-screen tests for native dropdown

* refactor profile-screen tests for native dropdown

* refactor thread-muting tests for native dropdown

* refactor thread-screen tests for native dropdown

* fix dropdown color for post dropdown button

* remove icons from android dropdown menu

* fix `create-account.test.ts`

* fix `invite-codes.test.ts`
2023-07-28 16:00:37 -05:00
Foysal Ahamed eec300d772 List cleanup on remove (#1069)
* 💄 Hide Add to List option on own profile

*  Remove Lists tab when last list is removed

*  Add listener to list delete on profile screen

*  Only show save changes in list modal when changes are made
2023-07-28 11:04:27 -05:00
Ansh 38d78e16bf Search custom feeds (#1031)
* paginate custom feeds

* basic search

* update `@atproto/api`

* use search from the API

* debounce search for 200ms
2023-07-28 10:29:37 -05:00
Foysal Ahamed 8e9b8b6b36 💄 Hide Add to List option on own profile (#1068) 2023-07-27 13:07:02 -05:00
Paul Frazee 49356700c3 [APP-782] Support invalid handles correctly (#1049)
* Update profile link construction to support handle.invalid

* Update list links  to support using handles

* Use did for isMe check to ensure invalid handles dont distort the check

* Shift the red (error) colors away from the pink spectrum

* Add ThemedText helper component

* Add sanitizedHandle() helper to render invalid handles well

* Fix regression: only show avatar in PostMeta when needed

* Restore the color of likes

* Remove users with invalid handles from default autosuggests
2023-07-27 10:50:12 -05:00
Steven 5a0899b989 fix: invisible url when only a protocol (#1059)
* fix: invisible url when only a protocol

- Fixes https://github.com/bluesky-social/social-app/issues/1058

* fix: handle more cases when url is not http/https
2023-07-27 10:49:37 -05:00
David Buchanan 44b3929240 Update icons.tsx - Simplify SatelliteDishIcon paths (#1048)
This prevents rendering jank
2023-07-25 10:21:17 -05:00
GitStart c1580dae1f fix: ALT text entered in the compose dialog is erased when you press "Edit" again (#1054) 2023-07-25 09:47:26 -05:00
Logan Rosen 806be6f722 Bump react-native-paste-input; remove hack (#1051) 2023-07-25 09:45:38 -05:00
Ansh bb99a234e1 [APP-720] Paginate custom feeds (#1030)
* paginate custom feeds

* Fix loading state bug

* DRY code up
2023-07-21 17:39:06 -05:00
Eric Bailey bf00d49863 #995 use asAnchor on external embeds (#1043)
* use asAnchor on external embeds

* add noFeedback back, fix TouchableWithoutFeedback anchor handling
2023-07-21 17:17:35 -05:00
Ansh 7e1dac0439 fix mute list overflow bug (#1041) 2023-07-20 11:48:23 -07:00
Ansh 30ac9259c7 [APP-775] Add Welcome screen after account creation (#1038)
* add comments to step 1-3

* add onboarding screen

* add analytics for onboarding tracking

* fix useEffect

* change text

* change icon size

* put onboarding into bottom sheet modal instead of react navigation

* wip

* Simplify the type validation

* Fix: only trigger onboarding modal when account creation succeeds

* Add the 'session-ready' event which fires when the new session is stable

* Use the 'session-ready' event to trigger the onboarding modal

* update copy

* update copy

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-07-19 23:50:42 -07:00
Ansh 3517d9fa28 Hotfix mobile web styling issues (#1039)
* remove hardcoded height for bottom bar

* prevent overflow of tab bar on mobile web
2023-07-19 15:00:40 -07:00
Eric Bailey 302490cb31 wrap interval in runInAction (#1037) 2023-07-19 14:24:35 -05:00
Eric Bailey 0ae52e91ce #984 Updating indexedAt timestamps (#1024)
* add TimeElapsed util component, integrate into PostThreadItem

* integrate into posts

* use consistent naming

* use mobx and single interval for TimeElapsed
2023-07-19 12:16:57 -05:00
Eric Bailey 4515559b1a #929 Wrap PasteInput updates in a setTimeout (#1033)
* wrap PasteInput updates in a setTimeout

* just wrap the whole callback
2023-07-19 12:08:40 -05:00
Sam Brownstone abd2c8e695 Fix cursor not being a pointer on profile back button (#1022)
* Fix cursor not being a pointer on profile back button

* Change TouchableOpacity to Pressable

* switch back to TouchableWithoutFeedback

---------

Co-authored-by: Ansh Nanda <anshnanda10@gmail.com>
2023-07-18 15:40:36 -07:00
Eric Bailey a6bb38ee51 Merge pull request #1032 from bluesky-social/fix-1001-modal-scrolling
#1001 Modal content overflow
2023-07-18 16:46:22 -05:00
Eric Bailey 8a66af11ae Merge pull request #1011 from bluesky-social/eric/app-741-add-pre-commit-hook
add pre commit hook for linting/formatting
2023-07-18 16:45:24 -05:00
Eric Bailey 9ef54814f0 fix lint 2023-07-18 15:26:04 -05:00
Eric Bailey 1ca5792165 create explicit relationship between values 2023-07-18 15:18:56 -05:00
Eric Bailey 17deaaa7e7 use calc 2023-07-18 15:15:02 -05:00
Ansh Nanda b59ca5f343 undo commit 2023-07-18 13:05:17 -07:00
Ansh Nanda e0f30c716a test 2023-07-18 13:04:29 -07:00
Eric Bailey ea567362a1 Merge pull request #1025 from GitStartHQ/BLUESKY-OSS-8
[Web] A lot of post content in a modal causes parts of modal to become inaccessible
2023-07-18 14:57:21 -05:00
Paul Frazee 8d47e6f112 Merge branch 'therealjeffg-term_violation_addition' into main 2023-07-18 11:47:22 -05:00
Paul Frazee 9ac820baac Tweak copy 2023-07-18 11:47:10 -05:00
Paul Frazee ff61fc3c1d Merge branch 'term_violation_addition' of https://github.com/therealjeffg/social-app into therealjeffg-term_violation_addition 2023-07-18 11:43:14 -05:00
gitstart dea8b74580 fix hidden parts on modal 2023-07-18 07:05:57 +00:00
Eric Bailey aa4b79e77a handle failed custom feeds on home screen (#1012)
* handle failed custom feeds on home screen

* remove unnecessary edit

* make sure edits to pinned feeds are handled and reloaded

* fix lint
2023-07-17 17:31:56 -05:00
Paul Frazee bfed96bd7b Bump ios build number 2023-07-17 16:17:49 -05:00
Paul Frazee ad15ee31f3 [APP-773] Change waitlist explainer copy (#1021)
* Change waitlist explainer copy

* Restore the waitlist modal snap point to 80% to account for smaller phones

* Tune copy further
2023-07-17 16:17:14 -05:00
Jeff Griffiths 76aa792afd adds additional reason for reporting an account profile 2023-07-14 09:50:55 -07:00
Eric Bailey 2f63afc92e fix typo 2023-07-13 17:39:43 -07:00
Eric Bailey c75383bf9c move eslint dep to devDeps 2023-07-13 15:56:29 -07:00
Eric Bailey 5a2401ada9 conditionally install husky 2023-07-13 15:55:30 -07:00
Eric Bailey c83573bf97 set up lint-staged via husky
(cherry picked from commit 98151fafbd9b248fce62f013cecd3d20d4d17006)
2023-07-13 15:35:56 -07:00
Paul Frazee 3bd891b01a 1.41 2023-07-07 12:01:31 -05:00
Paul Frazee 237e957d16 Fixes and improvements to the Profile Preview modal (#992)
* Fix: use more reliable navigation method

* Fix: show lightbox over the active modal

* Fix: close the profile preview on navigation

* Factor out UserPreviewLink and add preview behavior to notifications

* Fix postmeta overflow on web

* Fix lint
2023-07-07 12:00:17 -05:00
Paul Frazee d8aded7b15 1.40 2023-07-06 21:24:01 -05:00
Paul Frazee 6f69157269 Post UI updates (Profile Preview on mobile) (#990)
* Update postmeta to put the timestamp on the right side on mobile

* Drop the two-line PostMeta mode

* Add ProfilePreview modal

* Tune PostMeta to give the best behavior possible for a given platform

* Remove old showFollowBtn attributes

* Fix style issue

* Switch the follow button in the profile header to use the inverted color for consistency with the rest of the app

* Fix lint

* Fix darkmode

* Tune the profile preview footer

* Better analytics choice
2023-07-06 21:12:54 -05:00
Ansh df7552135a [APP-703] Android horizontal scroll registers as tap (#960)
* use Touchables from react-native-gesture-handler

* upgrade `react-native-gesture-handler` to latest version

* add FixedTouchableHighlight for android

* add workaround comment

* wait for animations to complete before loading data

* downgrade RNGH back to the version we had
2023-07-06 20:41:27 -05:00
Paul Frazee bf1785765d [APP-729] Replace the ImageHider blurring effect with a simpler and more reliable card (#959)
* Replace the ImageHider blurring effect with a simpler and more reliable card

* A few improvements to ImageHider layout

* Simplify the ImageHider a bit more

* Small web layout tweak
2023-07-06 20:29:52 -05:00
Ansh b06304a253 [APP-28] add status page link (#987)
* add status page to mobile

* add status page URL to super slow loading screen

* store STATUS_PAGE_URL in constants.ts
2023-07-06 20:29:18 -05:00
Paul Frazee 831b367eb9 [APP-736] Clarify hate-group moderation as it's currently used (#985)
* Clarify hate-group moderation as it's currently used

* Update src/lib/labeling/const.ts

Co-authored-by: Emily Liu <emilyliu7321@gmail.com>

---------

Co-authored-by: Ansh <anshnanda10@gmail.com>
Co-authored-by: Emily Liu <emilyliu7321@gmail.com>
2023-07-06 20:28:31 -05:00
Paul Frazee e14c9783e0 [APP-735] Post language improvements (#982)
* Fix composer character-counter bouncing around UI elements

* Fix composer toolbar padding when keyboard is dismissed on iOS

* Use the full name of the language in the composer footer

* Add headings to the DropdownButton

* Update the composer language control to use a simpler dropdown

* Fix lint

* Add translate link to Post component used in notifications

* Fix lint
2023-07-06 20:28:10 -05:00
Jonty Wareing f05c2f06d6 Fix missing post text in noscript post view (#981) 2023-07-06 12:12:00 -05:00
Paul Frazee edb37e2c06 1.39 2023-07-05 21:22:04 -05:00
Paul Frazee d442b1cd4c Update support links (#977)
* Add the new zendesk feedback form URL

* Dont wrap handle or display name in the drawer

* Add help link

* Tune drawer footer layout

* Type fixes

* make helpdesk urls more composable for future

* fix typo

---------

Co-authored-by: Ansh Nanda <anshnanda10@gmail.com>
2023-07-05 19:58:29 -05:00
Paul Frazee 3a6073abb8 Added instructions for .well-known method (supersedes #887) (#979)
* Added instructions for .well-known method

* Factor out SelectableBtn

* Rework the ChangeHandle modal to be a little clearer

* Fix lint

* Fix desktop layout

---------

Co-authored-by: Haider Ali Punjabi <haiderali@cyberservices.com>
Co-authored-by: Haider Ali Punjabi <haideralipunjabi@hackesta.org>
2023-07-05 19:56:42 -05:00
Ansh fe32730025 [APP-702] Render Custom Feed likes properly (#980)
* render href for custom feed likes properly

* fix custom feed text in push notification

* fix custom feed accessibility title

* fix lint
2023-07-05 19:56:26 -05:00
Ændra Rininsland 99aa38e3ca Fixes vertical alignment in feed tab bar (#974)
The tab bar items need `justify-content: center` on them or else they can appear unaligned when a feed name has an emoji character in it.
2023-07-05 18:26:18 -05:00
Ikko Eltociear Ashimine 3498c1d239 fix: typo in feed-manip.ts (#967)
prefered -> preferred
2023-07-05 09:49:45 -07:00
Paul Frazee 9750c393a0 1.38 2023-07-03 19:05:54 -05:00
Paul Frazee c8eeb6ba1c Fix: re-add the scroll boundary to avoid minimal shell at top of screen (#956) 2023-07-03 18:36:49 -05:00
Ansh 40a872612f don't throw, but log OTA listener errors (#958) 2023-07-03 18:17:28 -05:00
Ansh 269c0687fd track successful account creation (#955) 2023-07-03 18:17:03 -05:00
Paul Frazee 343154a403 Fix reply-root reference (fixes thread muting) (#957) 2023-07-03 17:54:09 -05:00
Paul Frazee 696bffe832 Add alt text validation option to user preferences (supersedes #913) (#914)
* Add alt text validation option to user preferences

* Fix typos/linting issues

* Update accessibility setting to match styles

* Update the required alt text reminder to go away once it's added

---------

Co-authored-by: Emma Fuller <emma@emmafuller.dev>
2023-07-03 15:58:07 -05:00
Paul Frazee bc55241c9a [APP-724] Collection of accessibility fixes (#949)
* Fix: include alt text on the web lightbox image

* a11y: Dont read the 'ALT' label

* a11y: remove a wrapper behavior from posts

This appears to have been introduced with the goal of creating meta
actions on posts, but the behavior seems counter-productive. The
accessibility inspector was unable to access individual items within
the post and therefore most content was simply skipped.

There may be a way to support the post actions without losing the
ability to access the inner elements but I couldnt find it. -prf

* a11y: apply alt tags to image wrappers so they get read

* a11y: set Link accessibilityLabel to the title if none set

* a11y: skip the SANDBOX watermark

* a11y: improve post meta to not read UI and give a useful date

* ally: improve post controls

* a11y: add labels to lightbox images on mobile

* fix types
2023-07-03 15:57:53 -05:00
Paul Frazee 0163ba0af8 Fix: let images on desktop web take more vertical space (#953) 2023-07-03 15:57:39 -05:00
Ansh edff1992b1 Add permission checks before saving image (#945)
* catch permission errors when saving image to album

* Save photos to media library alone (not an album)

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-07-03 13:17:12 -05:00
Paul Frazee 7cf0db7d14 Fix saved feeds screen layout on web (close #920) (#943) 2023-07-02 15:59:07 -05:00
Markus Amalthea Magnuson 1ea2a94b9b Fix keyboard navigation on settings page. (#923) 2023-07-02 15:18:42 -05:00
Markus Amalthea Magnuson 6768e3ff71 Make sidebar avatar keyboard-clickable. (#921) 2023-07-02 15:07:39 -05:00
Paul Frazee 97f0d3431c fix: stabilize minimalShellMode toggling by introducing modified thresholds (supersedes #938) (#942)
* add separate scroll up/down thresholds to prevent rapid state changes

* set `DY_LIMIT_DOWN` to 150

* Use a different scroll dy limit on web/mobile

---------

Co-authored-by: Sehyun Chung <sehyun.chung@icloud.com>
2023-07-02 15:03:16 -05:00
Ansh 7ee7d968e5 [APP-708] Onboarding revamp (#939)
* Move Discover New Feeds button to the top of the MultiFeed

* add discover custom feeds to empty following page
2023-07-02 13:30:02 -05:00
Dylan Smith 2be2498940 Use consistent font weight for repost count (#940) 2023-07-02 13:25:47 -05:00
Paul Frazee 7597fab290 1.37 2023-07-01 14:30:36 -05:00
Paul Frazee 2e93e4a919 Performance improvement (#937)
* Perf: Wait until a feed is visible in the home screen before fetching data

* perf: update feed checkForLatest to use limit=1

* Fix lint
2023-07-01 14:25:13 -05:00
Paul Frazee 93689ad73d Fix: correctly handle the case where the user has no languages configured 2023-06-30 12:21:30 -05:00
Paul Frazee 948b324cd8 1.36 2023-06-30 12:07:46 -05:00
Paul Frazee d50ba22820 Fix: reset navigation state on account switch (#933) 2023-06-30 11:38:37 -05:00
Paul Frazee 010588ee7c Fix thread muting (#932) 2023-06-30 11:38:22 -05:00
Paul Frazee ed5a88d9d8 [APP-718] Improvements and fixes to language handling (#931)
* Add locale helpers for narrowing languages

* Add a translate link to posts in a different language

* Update language filtering to use narrowing when multiple declared

* Fix a few more RTL layout cases

* Fix types
2023-06-30 11:35:29 -05:00
Paul Frazee 48844aa4c7 Include a notice during account creation for under-18 users (#927) 2023-06-30 11:34:52 -05:00
Paul Frazee c72e24f841 [APP-716] Add 'save image' button to the lightbox (#926)
* Add 'save image' button to the lightbox

* Fix types

* Fix types
2023-06-30 11:34:04 -05:00
Paul Frazee 5fcca17129 1.35 2023-06-28 13:41:42 -05:00
Paul Frazee eb05c4cd6f [APP-692] Accessibility font-scaling fixes (#922)
* Fix: enable font scaling in the composer text input

* Fix: cancel button in search no longer wraps

* Fix: report modals no longer clip off the bottom with scaled fonts

* Fix scrolling on android

* Allow wrapping in dropdown items until we can handle proper sizing
2023-06-28 13:38:21 -05:00
Paul Frazee df3589f207 Fix: hide the keyboard before showing the discard modal (#925) 2023-06-28 13:37:19 -05:00
Paul Frazee 8d32f3de37 Merge branch 'main' of github.com:bluesky-social/social-app into main 2023-06-28 09:59:16 -05:00
Paul Frazee c98bdcf86d 1.34 2023-06-27 20:56:45 -05:00
Paul Frazee f8d218e11a Simulator fixes (#918)
* Update the mock server to use the dev-env to manage the server

* Fix list testIDs

* Fix the invite test construction

* Remove leftover test hardcode
2023-06-27 20:55:46 -05:00
Paul Frazee dce80be075 Fix copy and dark mode styles in the home feed preferences (#915) 2023-06-27 15:00:00 -05:00
Paul Frazee 0921dd4335 Fix some broken links on policy docs (#916) 2023-06-27 14:59:42 -05:00
Paul Frazee a733b5b9f4 Revert change to the cancel button in the composer (#917) 2023-06-27 11:56:26 -07:00
Paul Frazee 1024f37a54 Merge branch 'benharri-forks-skip-confirmation-for-empty-post' into main 2023-06-27 11:04:33 -05:00
Paul Frazee 0f9429605d Merge branch 'skip-confirmation-for-empty-post' of https://github.com/benharri-forks/social-app into benharri-forks-skip-confirmation-for-empty-post 2023-06-27 11:01:25 -05:00
Ansh a8bbaa06c7 [APP-705] Metrics revamp pt2 (#896)
* export track function from analytics.tsx

* fix create account tracking

* fix tracking sign in

* add custom feed events

* fix type errors

* refactor create post event

* add profile follow & unfollow events

* refactor PostsFeedSliceModel into its own file

* refactor PostThreadItemModel into its own file

* reorganize code a lil bit

* refactor post-thread-item to use post-feed-item model under the hood

* add post events

* add post reply tracking

* track custom feed load more

* track list subscribe and unsubscribe
2023-06-27 10:11:05 -05:00
Paul Frazee bfaa6d73f3 Improvements to the alt text behaviors in the composer (#910)
* Add an image preview in the alt modal

* Composer: add info about alt text and a green checkmark when done

* Shrink the alt visual indicator a bit so it doesnt obscure the image

* Fix typo

* Fix: avoid requiring multiple tabs to save alt text

* update react-native-screens

* Improve the alt text help tip

* Remove redundant hints

---------

Co-authored-by: Ansh Nanda <anshnanda10@gmail.com>
2023-06-27 09:52:49 -05:00
Ansh 25b3e14926 make discard btn more apparent in UI (#912) 2023-06-26 17:10:04 -07:00
Paul Frazee cd21a7dfc9 Fix Voice Control input on iOS (closes #891) (#911)
* Add patch to fix issue with VoiceControl on iOS

* Fix .patch
2023-06-26 18:53:35 -05:00
Ben Harris 2e082b6977 fix cancel post behavior
- prompt now appears for the cancel button
- no prompt when post is empty
- appease eslint
2023-06-26 16:28:28 -04:00
Ansh b9abd444e5 [APP-657] Add share list functionality (#863)
* replace delete list button text with icon

* fix mute list styling on desktop

* add share button to nav bar on a list

* fix styling when on profile

* bug: add key to ImageHorzList

* clean up code & refactor

* fix styling for ListItems

* create a reusable ListActions component for actions on a list

* remove dead styles

* add keys to ListActions

* add helpers to set list embed

* render list embeds

* fix list sharing on web

* make style prop optional in ListCard

* update `@atproto/api` to `0.3.13`
2023-06-26 12:15:39 -05:00
Markus Amalthea Magnuson 1666a747eb Add a11y details to profile page compose button. (#909)
This adds accessibility role, label and hint to the compose button on profile pages, to match the same button in all other views.
2023-06-26 12:14:51 -05:00
Ansh 08804f265e [APP-690] better handling of post languages language filtering (#893)
* add SelectLangBtn

* memoized objects that are created to reduce re-creation on re-render

* add langs when uploading post

* only send the top 3 languages otherwise backend will throw error

* mv ContentLanguagesSettings to folder

* add post languages settings modal and state

* fix typos

* modify feed manip to also check langs label on post

* Fix tests

* Remove log

* Update feed-manip.ts

* Fix syntax errors

* UI tuneups

* Show the currently selected languages in the composer

* fix linting

* Use a bcp-47 matching function

* Fix a duplicate language issue

* Fix web

* Dont include lang in prompt

* Make select language btn an observer

* Keep device languages on top of language selection UIs

* Fix android build settings

* Enforce a max of 3 languages in posts

* Fix tests

* Fix types

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-06-23 12:48:52 -05:00
Logan Rosen 9b19a95e63 Bump @types/react to ^18 and adjust types (#889) 2023-06-22 12:40:32 -04:00
Paul Frazee 180697b66b 1.33 2023-06-16 16:28:46 -05:00
Paul Frazee 46c65958bc Merge branch 'estrattonbailey-eric/reply-settings-toggle' into main 2023-06-16 16:02:56 -05:00
Paul Frazee 20cdede5e1 Small style fixes 2023-06-16 16:02:26 -05:00
Paul Frazee 31c9e02c0a Update feed settings icon 2023-06-16 15:56:27 -05:00
Eric Bailey 20f77ae7b7 feat: styling/copy cleanup 2023-06-16 15:17:09 -05:00
Eric Bailey 73c6afc7fb fix: comment 2023-06-16 15:16:30 -05:00
Eric Bailey 896aea9837 feat: add home feed preferences settings modal and tuners 2023-06-16 15:01:43 -05:00
Ansh 17e7590bcd [APP-511] metrics overhaul: frontend work (#506)
* WIP

* fix types and update imports

* wip

* tagged events that should be server side

* remove server-side analytics

* remove useless import

* add additional profile header events

* remove useless import

* track follow/unfollow clicks

* add missing types
2023-06-15 16:45:14 -05:00
Paul Frazee 1695ae34db 1.32 2023-06-14 23:17:25 -05:00
Paul Frazee 56ec3bf89e Move policy docs to blueskyweb.xyz (#881) 2023-06-14 20:00:28 -05:00
Paul Frazee 3663ee57f3 Add testnet warning (#880)
* Add testnet warning

* Add watermarks to posts

* Call the test environment the Sandbox
2023-06-14 20:00:16 -05:00
Ansh 775b5e6578 add checks to make sure we are on web (#879) 2023-06-14 17:20:46 -05:00
Tymoteusz Boba 272ddeb7e2 Upgrade Reanimated from 2.14.4 to 3.1.0 (#753)
* Update Reanimated API to v3

* Fix failing tests after updating Reanimated

* Include changes from package.json in Reanimated library to patch

* upgrade reanimated to 3.3.0

* remove patch

* upgrade bottom sheet

---------

Co-authored-by: Ansh Nanda <anshnanda10@gmail.com>
2023-06-14 16:35:06 -05:00
Ansh 9b9f339e3e [APP-686] Fix mobile web styling (#876)
* fix desktop window size

* fix desktop window size

* add listener for resize events

* fix when going from mobile to web

* only add eventListener for web
2023-06-14 15:26:44 -05:00
Ansh 71af9fd04b add error handling to updateCache for saved-feeds (#878) 2023-06-14 15:14:56 -05:00
Markus Amalthea Magnuson c060cd4158 Various case and text fixes. (#757)
* Various case and text fixes.

* Update some labels per requested changes.

---------

Co-authored-by: Ansh <anshnanda10@gmail.com>
2023-06-13 11:35:13 -07:00
Ivan Silvestre 37782ae60b Desktop: prevent unnecessary X scroll between 1230px and 1260px (#828)
* Desktop: prevent unnecessary X scroll between 1230px and 1260px

* imports structure without changes

* re add dot to text
2023-06-13 11:29:36 -07:00
GitStart c20f8937cb UX: account deletion dialogue does not tell me which account I'm deleting (#835)
* fix: account deletion dialogue does not tell me which account I'm deleting

* update styles and display name
2023-06-13 11:24:57 -07:00
Paul Frazee 1a12fa5775 Add temporary appview-proxy header toggle (#874) 2023-06-13 13:10:42 -05:00
Paul Frazee 6efbe820d9 Merge branch 'loganrosen-loganrosen/heading-elements' into main 2023-06-12 17:26:17 -05:00
Paul Frazee bc2bf20e08 Update info screen titles to use H1 2023-06-12 17:25:52 -05:00
Paul Frazee 9c8e0ab33a Merge branch 'loganrosen/heading-elements' of https://github.com/loganrosen/social-app into loganrosen-loganrosen/heading-elements 2023-06-12 17:06:39 -05:00
Paul Frazee abdb4aace6 Merge branch 'main' of github.com:bluesky-social/social-app into main 2023-06-12 17:06:17 -05:00
Markus Amalthea Magnuson 571285c2b4 Fix image-only posts not being visible in custom feeds. (#871) 2023-06-12 17:05:31 -05:00
Logan Rosen 9232daba7a Move useWebMediaQueries() above return (#872) 2023-06-11 20:12:50 -07:00
Markus Amalthea Magnuson 8f74c7b949 Fix image-only posts not being visible in custom feeds. 2023-06-12 01:40:59 +02:00
Logan Rosen 55cf53cfd5 Use native heading elements 2023-06-10 19:12:26 -04:00
Paul Frazee 8d03ab9399 1.31 2023-06-09 17:32:40 -05:00
Logan Rosen 9ac8c44a6a Use Linking.openSettings() so it works on Android (#865) 2023-06-08 20:47:24 -07:00
Logan Rosen cae615769c Link to instructions for creating Sentry auth token (#864) 2023-06-08 19:54:37 -07:00
Ansh 6a967cce00 make build version text copy on press (#862) 2023-06-08 14:40:20 -05:00
Ansh b3e7801796 Revert "show date after 7 days closes #754" (#860) 2023-06-08 11:32:56 -05:00
Ansh 007d12e182 push instead of navigate (#861) 2023-06-07 17:55:25 -05:00
Ansh 0be14a1b46 [APP-680] Allow users to add details when reporting (#854)
* allow user to add text when reporting post

* add DMCA override

* increase modal size

* fix dark mode text color

* re-organize components

* add details option when reporting account

* hard-code modal size so it works on smaller devices

* fix modal on web

* Remove outline from textarea focus

* Tweak some styles

* Fix lint

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-06-07 11:11:04 -05:00
Ansh fc12a1205c allow image to clicked to go to post in notificaitons (#858) 2023-06-07 09:57:49 -05:00
Logan Rosen bdcdb4e4dc Use expo-image-picker on Web (#847) 2023-06-06 09:38:58 -05:00
Keith Cirkel a67eaa6ace remove X-UA-Compatible meta tag (#857)
This meta tag allows Internet Explorer 7 through 11 to change which document mode they would use. As of 2013, however, [Microsoft Edge would ignore this value except under strict circumstances](https://learn.microsoft.com/en-gb/archive/blogs/ie/living-on-the-edge-our-next-step-in-helping-the-web-just-work). This meta tag is ignored in modern versions of Edge (Chromium based).

[Internet Explorer 11 reached EOL in June 2022](https://blogs.windows.com/windowsexperience/2022/06/15/internet-explorer-11-has-retired-and-is-officially-out-of-support-what-you-need-to-know/) and today makes up an exceedingly small proportion of internet traffic.

It's unlikely that anyone is browsing on Internet Explorer, and less
likely that they're browsing on IE<11, which makes this tag redundant.

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-06-06 09:33:23 -05:00
Keith Cirkel b33626c3c7 remove trailing / on meta tags (#856)
* remove trailing / on meta tags

Meta tags are [void elements](https://html.spec.whatwg.org/multipage/syntax.html#void-elements) which means they are self closing, and do not require a closing tag, nor a trailing `/`.

The trailing `/` is something that was required in XHTML but [is ignored
in HTML](https://html.spec.whatwg.org/multipage/syntax.html#start-tags). Quoth the spec:

> On void elements, [the trailing slash] does not mark the start tag as self-closing but instead is unnecessary and has no effect of any kind. For such void elements, it should be used only with caution — especially since, if directly preceded by an unquoted attribute value, it becomes part of the attribute value rather than being discarded by the parser.

* remove trailing / on link tags
2023-06-06 09:32:30 -05:00
Ansh bf36101bf2 [APP-655] Password autocomplete when logging in (#838)
* remove unused styles

* refine login form to autofill password from password manager
2023-06-06 09:31:50 -05:00
Logan Rosen 8cc89a5a16 Fix dark mode detection on mobile (#852) 2023-06-05 09:40:13 -07:00
Paul Frazee 48637246c7 1.30 2023-06-02 15:38:49 -05:00
Paul Frazee 75b729a670 Merge branch 'main' of github.com:bluesky-social/social-app into main 2023-06-02 15:29:36 -05:00
Paul Frazee a2721ceb95 Fix to simulator tests 2023-06-02 15:29:27 -05:00
Ansh ba4bb46c3f [APP-107] OTA updates (#587)
* add 1000ms fallbackToCacheTimeout

* add listener via useOTAUpdate hook and show modal if update is available

* finish expo-updates setup

* setup useOTAUpdate hook

* add 1000ms fallbackToCacheTimeout

* add listener via useOTAUpdate hook and show modal if update is available

* finish expo-updates setup

* setup useOTAUpdate hook

* add OTA updates

* Update build.md

* temporarily disable ota updates

* refactor useOTAUpdate code
2023-06-02 13:27:59 -07:00
Paul Frazee ad4eaf5ed2 Move suggested follow recommendations to the server (#836)
* Move suggested follow recommendations to the server

* fix search results color in dark mode

---------

Co-authored-by: Ansh Nanda <anshnanda10@gmail.com>
2023-06-02 15:01:16 -05:00
Paul Frazee e8843ded5b Fix a bunch of type errors and add a type-check to the github workflows (#837)
* Add yarn type-check

* Rename to yarn typecheck

* Fix a collection of type errors

* Add typecheck to automated tests

* add `dist` to exluded folders tsconfig

---------

Co-authored-by: Ansh Nanda <anshnanda10@gmail.com>
2023-06-02 15:01:04 -05:00
Logan Rosen 46c9de7c18 Don't show Remove button for nonexistent avatar/banner (#833) 2023-06-02 09:51:11 -05:00
Paul Frazee 3217c7ff32 More custom-feed behavior fixes [APP-678] (#831)
* Remove extraneous custom-feed health check

* Fixes to custom feed preference sync

* Fix lint

* Remove dead code (client-side suggested posts constructor)

* Enforce the feed-fetch limit in the client if the generator fails to observe the parameter

* Bump the number of items fetched in the multifeed per feed from 5 to 10

* Reset the currently active feed when the pinned feeds change

* Some fixes to icons

* Add a prompt to load latest to the multifeed

* Remove debug
2023-06-02 09:48:53 -05:00
Paul Frazee e9c84a192b Fixes to feed preference and state sync [APP-678] (#829)
* Remove extraneous custom-feed health check

* Fixes to custom feed preference sync

* Fix lint

* Fix to how preferences are synced to enable membership modifications
2023-06-01 14:46:13 -05:00
Jake Gold f416798c5f bskyweb: add security.txt 2023-06-01 11:47:59 -07:00
Kadi Kraman d4e7355cca fix: support scroll to top on profile screen (#725)
* Support scroll to top on profile screen

* Refactor types

* Remove async

* Improve types
2023-06-01 11:00:00 -05:00
Michi Hoffmann 792d7e1a55 Fix line breaks on side bar links (#815) 2023-06-01 10:51:42 -05:00
Samuel Newman 88212eb065 translate to primary language rather than just english (#814) 2023-06-01 10:50:18 -05:00
Ansh 1fc694d488 [APP-656] make alt text expandable (#821)
* make alt text expandable

* clean code
2023-06-01 10:47:17 -05:00
Ansh 5c25d670d0 move early return to after all hooks (#823) 2023-06-01 10:41:27 -05:00
Ansh 54e5e30945 [APP-674] Fix search results tab bar (#822)
* fix search results tab bar

* Fix spacing on web

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-06-01 10:40:26 -05:00
Jake Gold 49840f3a27 bskyweb: gzip HTTP responses + some other minor improvements (#826)
* bskyweb: gzip HTTP responses + JSON logging + minor refactoring

* reduce timeout and max header size

* add a security.txt
2023-06-01 10:22:02 -05:00
Ansh 8fde55b59b simplify how x position is calculated for scrollTo in TabBar (#820) 2023-06-01 10:19:15 -05:00
bnewbold 31a41d9b09 bskyweb: run-dev-bskyweb in debug mode (#827) 2023-05-31 20:44:50 -07:00
Ansh 189fdb1789 fix linting & type errors (#819) 2023-05-31 17:19:08 -07:00
LW 7d7410942f fix: make DesktopLeftNav not overflow screen (#762)
* fix: make DesktopLeftNav not overflow screen

* scroll: auto
2023-05-30 21:18:00 -05:00
Ansh 3cb8169a4f Fix deeplink when app is suspended (#812)
* Update Navigation.tsx

Missing If statement within the Deep Linking function that broke the ability to change state to hometab

* Updated the function correctly, I missed a bracket

* format

* add pre-existing home state when unspecified path

* Remove unnecessary log

---------

Co-authored-by: Gai <68623807+that-gai-gai@users.noreply.github.com>
Co-authored-by: that-gai-gai <kgai@protonmail.com>
2023-05-30 21:16:29 -05:00
Paul Frazee 421f29ad98 Merge branch 'ericvolp12-inherit_system_theme' into main 2023-05-30 21:13:34 -05:00
Paul Frazee 2413549fa5 Tune sizing of appearance control 2023-05-30 21:13:18 -05:00
Jaz 09ade363fd Merge branch 'main' into inherit_system_theme 2023-05-30 18:25:29 -07:00
38elements e224569a11 Remove lodash.clonedeep in package.json (#652) 2023-05-30 20:19:09 -05:00
Matin Zadeh Dolatabad fa9351534b perf: enable lazy loading for tab navigation (#711) 2023-05-30 20:16:30 -05:00
Markus Amalthea Magnuson 7d47b342d6 Make terms and notices dates less ambiguous. (#713) 2023-05-30 20:09:35 -05:00
Ollie H c0ca27b7ce Use proxy for fetching link meta (#716)
* Use proxy for fetching link meta

* Remove link meta test due to hitting proxy

* setup different staging and prod proxy URLs

---------

Co-authored-by: Ansh Nanda <anshnanda10@gmail.com>
Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-05-30 20:08:49 -05:00
Bartosz Kaszubowski 2018558585 feat(Login): allow submitting on password field (#720) 2023-05-30 19:56:13 -05:00
Ben Harris 037713321d show package.json version on web (#717) 2023-05-30 19:53:23 -05:00
Ollie H a9a661ab58 Add ability to reply, repost (without quote post), and like posts using VoiceOver (#765)
Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-05-30 19:50:56 -05:00
Ben Harris 7458b6f600 Move MAX_GRAPHEME_LENGTH to constants.ts (#735) 2023-05-30 19:41:56 -05:00
skipness 0239225796 fix: content trimmed in server input modal (#733) 2023-05-30 19:41:21 -05:00
Okechukwu Somtochukwu 8258317c38 [typo] correct typo was 'fo' instead of 'to' (#721) 2023-05-30 17:26:40 -07:00
Ollie H 072682dd9f Rework scaled dimensions and compression (#737)
* Rework scaled dimensions and compression

* Unbreak image / banner uploads

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-05-30 19:23:55 -05:00
Ollie H deebe18aaa Use non-pipe translation link (#740) 2023-05-30 18:54:47 -05:00
Ansh a911250ea8 add instructions to removing sentry in build.md (#749)
Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2023-05-30 18:50:52 -05:00
David Buchanan 1ca40cf1fd Update HomeIconSolid: widen the front door (#756) 2023-05-30 18:49:03 -05:00
Ben Harris ca34364cf0 Support arrow key navigation for Lightbox.web.tsx (#761)
* Support arrow key navigation for Lightbox.web.tsx

renames onEscape to onKeyDown

* appease eslint

* appease eslint again

* wrap onPressLeft and onPressRight in useCallback
2023-05-30 18:45:49 -05:00
MohammadReza Mahmoudi 442d453600 Fix profile load issue + enhancement of profile isCurrent style (#764)
* fix profile isActive issue

* fix myProfile load issue when visiting other profiles first

* fix lint issues

* change currentRouteName to currentRouteInfo
2023-05-30 18:42:22 -05:00
Paul Frazee 5d0a3e6c65 Merge branch 'main' of github.com:bluesky-social/social-app into main 2023-05-30 18:35:18 -05:00
Seth Arnold 1e5c4609a8 Update ProfileHeader.tsx (#810)
Change error message from "failed to blocK" to "failed to unblock"
2023-05-30 18:32:46 -05:00
Paul Frazee a11a1b7657 Merge branch 'show_timestamp_after_7d' of https://github.com/benharri-forks/social-app into main 2023-05-30 18:20:26 -05:00
Paul Frazee 762ab3d385 Update yarn.lock 2023-05-30 18:20:11 -05:00
Ben Harris 439135944c make eslint happy 2023-05-30 18:30:12 -04:00
Ben Harris f2cf1d8c79 show date after 7 days, closes #754
adjust the behavior of ago() to show the date after a week
2023-05-30 18:22:22 -04:00
Ollie H 234e04c396 Update follower/following a11y hints and labels (#755)
* Update follower/following a11y hints and labels

* Update ProfileHeader.tsx

---------

Co-authored-by: Ansh <anshnanda10@gmail.com>
2023-05-30 13:39:50 -07:00
Ben Harris 09c515d273 fix web links to no longer point to staging (#758)
* fix web link in README.md

* missed one
2023-05-30 13:34:14 -07:00
Josh Soref c808721938 Spelling (#772)
* spelling: account

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

* spelling: activated

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

* spelling: additional

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

* spelling: appropriate

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

* spelling: bskyweb

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

* spelling: description

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

* spelling: display

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

* spelling: highlighted

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

* spelling: javascript

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

* spelling: locally-hosted

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

* spelling: notification

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

* spelling: occurring

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

* spelling: parenthetical

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

* spelling: preexisting

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

* spelling: prefetched

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

* spelling: punctuation

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

* spelling: simplicity

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

---------

Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2023-05-30 14:46:43 -05:00
Ansh 724951e2f0 stick to bottom sheet version 4.4.5 (#777) 2023-05-30 14:37:40 -05:00
GitStart 69bcb72061 update delete app password confirmation modal (#803) 2023-05-30 13:56:30 -05:00
Logan Rosen a89129fcf6 Fix button to find people to follow (#806) 2023-05-30 13:55:06 -05:00
Paul Frazee 19ea6b6ae7 Merge branch 'ivansilvestre-main' into main 2023-05-30 13:53:46 -05:00
Paul Frazee e77c616970 Hide both the left and right nav on desktop when logged out 2023-05-30 13:53:33 -05:00
Paul Frazee 7a7b99d033 Merge branch 'main' of https://github.com/ivansilvestre/social-app into ivansilvestre-main 2023-05-30 13:45:44 -05:00
bnewbold a51d5dc7e2 labels: add 'corpse' to 'violent' category (#802)
Overlooked this when the label was added earlier.
2023-05-30 13:43:37 -05:00
Ivan Silvestre caab6309df Desktop: hide the left panel and the invite codes from the main page if you are not logged in 2023-05-30 13:54:23 +01:00
Jake Gold e58fe2e204 add/serve .well-known files (both files) 2023-05-26 17:59:25 -07:00
Jake Gold c98b65f6a9 add/serve .well-known files 2023-05-26 17:49:58 -07:00
Paul Frazee 7b6948e617 Merge branch 'custom-algos' into main 2023-05-25 21:17:11 -05:00
Ollie H 15c1b6ee15 Merge pull request #744 from bluesky-social/ollie/693
Update labels for avatar
2023-05-18 21:09:39 -07:00
renahlee 2a5ac1a6de Update labels for avatar 2023-05-18 17:31:57 -07:00
Jake Gold 211fce47ce Fix /api/waitlist API (#726)
* move /waitlist to /api/waitlist where its expected

* parse waitlist API request as JSON, duh
2023-05-18 11:56:09 -07:00
Jaz 7f76c2d67e Merge branch 'main' into inherit_system_theme 2023-05-17 21:19:20 -07:00
Jaz ac3a95dc72 Remove unused imports 2023-05-17 21:15:37 -07:00
Jaz 3c15f6ba02 Move appearance settings to settings page 2023-05-17 21:14:26 -07:00
Jaz b2ef6bde00 Unstage open post in new tab changes, oops 2023-05-16 23:22:18 -07:00
Jaz f4327aff69 Fix mobile layout at least for mobile web 2023-05-16 23:11:29 -07:00
Jaz 85dfef87ab Use a three-state radio button for color mode 2023-05-16 23:06:08 -07:00
Jaz da0ed7e002 Feat: Use system default color mode, but allow user override 2023-05-16 21:36:43 -07:00
430 changed files with 28838 additions and 18438 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ module.exports = {
simulator: {
type: 'ios.simulator',
device: {
type: 'iPhone 14',
type: 'iPhone 15 Pro',
},
},
attached: {
+102
View File
@@ -0,0 +1,102 @@
# OSX
#
.DS_Store
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
*.hprof
# node.js
#
node_modules/
npm-debug.log
yarn-error.log
# BUCK
buck-out/
\.buckd/
*.keystore
!debug.keystore
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/
*/fastlane/report.xml
*/fastlane/Preview.html
*/fastlane/screenshots
# Bundle artifact
*.jsbundle
# Ruby / CocoaPods
/ios/Pods/
/vendor/bundle/
# Testing
coverage/
junit.xml
artifacts
# Web bundle outputs
/dist/
# Expo
.expo/
dist/
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*
# VSCode
.vscode
# gitignore and github actions
!.gitignore
!.github
# Android & iOS folders
android/
ios/
# environment variables
.env
.env.*
# Firebase (Android) Google services
# INCLUDED: google-services.json
+16 -9
View File
@@ -2,24 +2,31 @@ module.exports = {
root: true,
extends: [
'@react-native-community',
'plugin:react/recommended',
'plugin:react-native-a11y/ios',
'prettier',
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'detox'],
plugins: ['@typescript-eslint', 'detox', 'react'],
rules: {
'react/no-unescaped-entities': 0,
'react-native/no-inline-styles': 0,
},
ignorePatterns: [
'**/__mocks__/*.ts',
'src/platform/polyfills.ts',
'src/third-party',
'ios',
'android',
'coverage',
'*.lock',
'.husky',
'patches',
'bskyweb',
'*.html',
'bskyweb',
],
overrides: [
{
files: ['*.js', '*.mjs', '*.ts', '*.tsx'],
rules: {
semi: [2, 'never'],
},
},
],
settings: {
componentWrapperFunctions: ['observer'],
},
}
@@ -12,6 +12,7 @@ env:
jobs:
bskyweb-container-aws:
if: github.repository == 'bluesky-social/social-app'
runs-on: ubuntu-latest
permissions:
contents: read
@@ -14,6 +14,7 @@ env:
jobs:
bskyweb-container-ghcr:
if: github.repository == 'bluesky-social/social-app'
runs-on: ubuntu-latest
permissions:
contents: read
+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
- name: Set up Go tooling
uses: actions/setup-go@v3
with:
go-version: '1.20'
go-version: '1.21'
- name: Dummy JS File
run: touch bskyweb/static/js/blah.js
- name: Check
@@ -36,7 +36,7 @@ jobs:
- name: Set up Go tooling
uses: actions/setup-go@v3
with:
go-version: '1.20'
go-version: '1.21'
- name: Dummy JS File
run: touch bskyweb/static/js/blah.js
- name: Lint
+5 -3
View File
@@ -17,9 +17,11 @@ jobs:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Yarn install
run: yarn
- name: Typescript & Lint check
run: yarn --frozen-lockfile
- name: Lint check
run: yarn lint
- name: Type check
run: yarn typecheck
testing:
name: Run tests
runs-on: ubuntu-latest
@@ -31,7 +33,7 @@ jobs:
- name: Check out Git repository
uses: actions/checkout@v3
- name: Yarn install
run: yarn
run: yarn --frozen-lockfile
- name: Run tests
run: |
yarn test --forceExit
+7 -1
View File
@@ -96,4 +96,10 @@ ios/
# environment variables
.env
.env.*
.env.*
# Firebase (Android) Google services
google-services.json
# Performance results (Flashlight)
.perf/
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn lint-staged
+4 -3
View File
@@ -1,4 +1,4 @@
FROM golang:1.20-bullseye AS build-env
FROM golang:1.21-bullseye AS build-env
WORKDIR /usr/src/social-app
@@ -13,11 +13,12 @@ ENV GODEBUG="netdns=go"
ENV GOOS="linux"
ENV GOARCH="amd64"
ENV CGO_ENABLED=1
ENV GOEXPERIMENT="loopvar"
COPY . .
#
# Generate the Javascript webpack.
# Generate the JavaScript webpack.
#
RUN mkdir --parents $NVM_DIR && \
wget \
@@ -36,7 +37,7 @@ RUN \. "$NVM_DIR/nvm.sh" && \
RUN find ./bskyweb/static && find ./web-build/static
#
# Generate the bksyweb Go binary.
# Generate the bskyweb Go binary.
#
RUN cd bskyweb/ && \
go mod download && \
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright 2023 Bluesky PBLLC
Copyright 2023 Bluesky PBC
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+25 -10
View File
@@ -1,24 +1,33 @@
# Bluesky Social App
Welcome friends! This is the codebase for the Bluesky Social app. It serves as a resource to engineers building on the [AT Protocol](https://atproto.com).
Welcome friends! This is the codebase for the Bluesky Social app.
- **Web: [staging.bsky.app](https://staging.bsky.app)**
Get the app itself:
- **Web: [bsky.app](https://bsky.app)**
- **iOS: [App Store](https://apps.apple.com/us/app/bluesky-social/id6444370199)**
- **Android: [Play Store](https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&hl=en_US&gl=US)**
Links:
## Development Resources
- [Build instructions](./docs/build.md)
- [ATProto repo](https://github.com/bluesky-social/atproto)
- [ATProto docs](https://atproto.com)
This is a [React Native](https://reactnative.dev/) application, written in the TypeScript programming language. It builds on the `atproto` TypeScript packages (like [`@atproto/api`](https://www.npmjs.com/package/@atproto/api)), code for which is also on open source, but in [a different git repository](https://github.com/bluesky-social/atproto).
## Rules & guidelines
There is a small amount of Go language source code (in `./bskyweb/`), for a web service that returns the React Native Web application.
---
The [Build Instructions](./docs/build.md) are a good place to get started with the app itself.
️ While we do accept contributions, we prioritize high quality issues and pull requests. Adhering to the below guidelines will ensure a more timely review.
The Authenticated Transfer Protocol ("AT Protocol" or "atproto") is a decentralized social media protocol. You don't *need* to understand AT Protocol to work with this application, but it can help. Learn more at:
---
- [Overview and Guides](https://atproto.com/guides/overview)
- [Github Discussions](https://github.com/bluesky-social/atproto/discussions) 👈 Great place to ask questions
- [Protocol Specifications](https://atproto.com/specs/atp)
- [Blogpost on self-authenticating data structures](https://blueskyweb.xyz/blog/3-6-2022-a-self-authenticating-social-protocol)
The Bluesky Social application encompasses a set of schemas and APIs built in the overall AT Protocol framework. The namespace for these "Lexicons" is `app.bsky.*`.
## Contributions
> While we do accept contributions, we prioritize high quality issues and pull requests. Adhering to the below guidelines will ensure a more timely review.
**Rules:**
@@ -52,6 +61,12 @@ Please be sure to:
If you discover any security issues, please send an email to security@bsky.app. The email is automatically CCed to the entire team and we'll respond promptly.
## Are you a developer interested in building on atproto?
Bluesky is an open social network built on the AT Protocol, a flexible technology that will never lock developers out of the ecosystems that they help build. With atproto, third-party can be as seamless as first-party through custom feeds, federated services, clients, and more.
If you're a developer interested in building on atproto, we'd love to email you a Bluesky invite code. Simply share your GitHub (or similar) profile with us via [this form](https://forms.gle/BF21oxVNZiDjDhXF9).
## License (MIT)
See [./LICENSE](./LICENSE) for the full license.
+77
View File
@@ -0,0 +1,77 @@
# flow.yaml
appId: xyz.blueskyweb.app
---
- launchApp
# Login
# - runFlow:
# when:
# - tapOn: "Sign In"
# - tapOn: "Username or email address"
# - inputText: "ansh.bsky.team"
# - tapOn: "Password"
# - inputText: "PASSWORd"
# - tapOn: "Next"
# Allow notifications if popup is visible
# - runFlow:
# when:
# visible: "Notifications"
# commands:
# - tapOn: "Allow"
# Scroll in main feed
- "scroll"
- "scroll"
- "scroll"
- "scroll"
- "scroll"
- "scroll"
- "scroll"
- "scroll"
# Swipe between feeds
- swipe:
direction: "LEFT"
- swipe:
direction: "LEFT"
- swipe:
direction: "LEFT"
- swipe:
direction: "RIGHT"
- swipe:
direction: "RIGHT"
- swipe:
direction: "RIGHT"
# Go to Notifications
- tapOn:
id: "viewHeaderDrawerBtn"
- tapOn: "Notifications"
- "scroll"
- "scroll"
- "scroll"
- "scroll"
- "scroll"
- swipe:
direction: "DOWN" # Make header visible
# Go to Feeds tab
- tapOn:
id: "viewHeaderDrawerBtn"
- tapOn: "Feeds"
- scrollUntilVisible:
element: "Discover"
direction: UP
- tapOn: "Discover"
- waitForAnimationToEnd
- "scroll"
- "scroll"
- "scroll"
- "scroll"
- "scroll"
# Click on post
- tapOn:
id: "postText"
index: 0
- "scroll"
- "scroll"
- "scroll"
- "scroll"
- "scroll"
+261 -44
View File
@@ -34,6 +34,9 @@ async function main() {
displayName: 'Carla',
description: 'Test user 3',
}))
if (inviteRequired) {
await server.mocker.createInvite(server.mocker.users.alice.did)
}
}
if ('follows' in url.query) {
console.log('Generating mock follows')
@@ -50,6 +53,10 @@ async function main() {
await server.mocker.users[user].agent.post({text: 'Post'})
}
}
if ('feeds' in url.query) {
console.log('Generating mock feed')
await server.mocker.createFeed('alice', 'alice-favs', [])
}
if ('thread' in url.query) {
console.log('Generating mock posts')
const res = await server.mocker.users.bob.agent.post({
@@ -63,6 +70,82 @@ async function main() {
},
})
}
if ('mergefeed' in url.query) {
console.log('Generating mock users')
await server.mocker.createUser('alice')
await server.mocker.createUser('bob')
await server.mocker.createUser('carla')
await server.mocker.createUser('dan')
await server.mocker.users.alice.agent.upsertProfile(() => ({
displayName: 'Alice',
description: 'Test user 1',
}))
await server.mocker.users.bob.agent.upsertProfile(() => ({
displayName: 'Bob',
description: 'Test user 2',
}))
await server.mocker.users.carla.agent.upsertProfile(() => ({
displayName: 'Carla',
description: 'Test user 3',
}))
await server.mocker.users.dan.agent.upsertProfile(() => ({
displayName: 'Dan',
description: 'Test user 4',
}))
console.log('Generating mock follows')
await server.mocker.follow('alice', 'bob')
await server.mocker.follow('alice', 'carla')
console.log('Generating mock posts')
let posts: Record<string, any[]> = {
alice: [],
bob: [],
carla: [],
dan: [],
}
for (let i = 0; i < 10; i++) {
for (let user in server.mocker.users) {
if (user === 'alice') continue
posts[user].push(
await server.mocker.createPost(user, `Post ${i}`),
)
}
}
for (let i = 0; i < 10; i++) {
for (let user in server.mocker.users) {
if (user === 'alice') continue
if (i % 5 === 0) {
await server.mocker.createReply(user, 'Self reply', {
cid: posts[user][i].cid,
uri: posts[user][i].uri,
})
}
if (i % 5 === 1) {
await server.mocker.createReply(user, 'Reply to bob', {
cid: posts.bob[i].cid,
uri: posts.bob[i].uri,
})
}
if (i % 5 === 2) {
await server.mocker.createReply(user, 'Reply to dan', {
cid: posts.dan[i].cid,
uri: posts.dan[i].uri,
})
}
await server.mocker.users[user].agent.post({text: `Post ${i}`})
}
}
console.log('Generating mock feeds')
await server.mocker.createFeed(
'alice',
'alice-favs',
posts.dan.map(p => p.uri),
)
await server.mocker.createFeed(
'alice',
'alice-favs2',
posts.dan.map(p => p.uri),
)
}
if ('labels' in url.query) {
console.log('Generating naughty users with labels')
@@ -72,26 +155,35 @@ async function main() {
)
for (const user of [
'csam-account',
'csam-profile',
'csam-posts',
'dmca-account',
'dmca-profile',
'dmca-posts',
'porn-account',
'porn-profile',
'porn-posts',
'nudity-account',
'nudity-profile',
'nudity-posts',
'scam-account',
'scam-profile',
'scam-posts',
'unknown-account',
'unknown-profile',
'unknown-posts',
'always-filter-account',
'always-filter-profile',
'always-filter-posts',
'always-warn-account',
'always-warn-profile',
'always-warn-posts',
'hide-account',
'hide-profile',
'hide-posts',
'no-promote-account',
'no-promote-profile',
'no-promote-posts',
'warn-account',
'warn-profile',
'warn-posts',
'muted-account',
'muted-by-list-account',
'blocking-account',
'blockedby-account',
'mutual-block-account',
]) {
await server.mocker.createUser(user)
await server.mocker.follow('alice', user)
@@ -105,25 +197,25 @@ async function main() {
await server.mocker.like(user, anchorPost)
}
await server.mocker.labelAccount('csam', 'csam-account')
await server.mocker.labelProfile('csam', 'csam-profile')
await server.mocker.labelAccount('dmca-violation', 'dmca-account')
await server.mocker.labelProfile('dmca-violation', 'dmca-profile')
await server.mocker.labelPost(
'csam',
await server.mocker.createPost('csam-posts', 'csam post'),
'dmca-violation',
await server.mocker.createPost('dmca-posts', 'dmca post'),
)
await server.mocker.labelPost(
'csam',
'dmca-violation',
await server.mocker.createQuotePost(
'csam-posts',
'csam quote post',
'dmca-posts',
'dmca quote post',
anchorPost,
),
)
await server.mocker.labelPost(
'csam',
'dmca-violation',
await server.mocker.createReply(
'csam-posts',
'csam reply',
'dmca-posts',
'dmca reply',
anchorPost,
),
)
@@ -132,7 +224,7 @@ async function main() {
await server.mocker.labelProfile('porn', 'porn-profile')
await server.mocker.labelPost(
'porn',
await server.mocker.createPost('porn-posts', 'porn post'),
await server.mocker.createImagePost('porn-posts', 'porn post'),
)
await server.mocker.labelPost(
'porn',
@@ -155,7 +247,7 @@ async function main() {
await server.mocker.labelProfile('nudity', 'nudity-profile')
await server.mocker.labelPost(
'nudity',
await server.mocker.createPost('nudity-posts', 'nudity post'),
await server.mocker.createImagePost('nudity-posts', 'nudity post'),
)
await server.mocker.labelPost(
'nudity',
@@ -174,6 +266,29 @@ async function main() {
),
)
await server.mocker.labelAccount('scam', 'scam-account')
await server.mocker.labelProfile('scam', 'scam-profile')
await server.mocker.labelPost(
'scam',
await server.mocker.createPost('scam-posts', 'scam post'),
)
await server.mocker.labelPost(
'scam',
await server.mocker.createQuotePost(
'scam-posts',
'scam quote post',
anchorPost,
),
)
await server.mocker.labelPost(
'scam',
await server.mocker.createReply(
'scam-posts',
'scam reply',
anchorPost,
),
)
await server.mocker.labelAccount(
'not-a-real-label',
'unknown-account',
@@ -203,54 +318,74 @@ async function main() {
),
)
await server.mocker.labelAccount('!filter', 'always-filter-account')
await server.mocker.labelProfile('!filter', 'always-filter-profile')
await server.mocker.labelAccount('!hide', 'hide-account')
await server.mocker.labelProfile('!hide', 'hide-profile')
await server.mocker.labelPost(
'!filter',
await server.mocker.createPost(
'always-filter-posts',
'always-filter post',
),
'!hide',
await server.mocker.createPost('hide-posts', 'hide post'),
)
await server.mocker.labelPost(
'!filter',
'!hide',
await server.mocker.createQuotePost(
'always-filter-posts',
'always-filter quote post',
'hide-posts',
'hide quote post',
anchorPost,
),
)
await server.mocker.labelPost(
'!filter',
'!hide',
await server.mocker.createReply(
'always-filter-posts',
'always-filter reply',
'hide-posts',
'hide reply',
anchorPost,
),
)
await server.mocker.labelAccount('!warn', 'always-warn-account')
await server.mocker.labelProfile('!warn', 'always-warn-profile')
await server.mocker.labelAccount('!no-promote', 'no-promote-account')
await server.mocker.labelProfile('!no-promote', 'no-promote-profile')
await server.mocker.labelPost(
'!no-promote',
await server.mocker.createPost(
'no-promote-posts',
'no-promote post',
),
)
await server.mocker.labelPost(
'!no-promote',
await server.mocker.createQuotePost(
'no-promote-posts',
'no-promote quote post',
anchorPost,
),
)
await server.mocker.labelPost(
'!no-promote',
await server.mocker.createReply(
'no-promote-posts',
'no-promote reply',
anchorPost,
),
)
await server.mocker.labelAccount('!warn', 'warn-account')
await server.mocker.labelProfile('!warn', 'warn-profile')
await server.mocker.labelPost(
'!warn',
await server.mocker.createPost(
'always-warn-posts',
'always-warn post',
),
await server.mocker.createPost('warn-posts', 'warn post'),
)
await server.mocker.labelPost(
'!warn',
await server.mocker.createQuotePost(
'always-warn-posts',
'always-warn quote post',
'warn-posts',
'warn quote post',
anchorPost,
),
)
await server.mocker.labelPost(
'!warn',
await server.mocker.createReply(
'always-warn-posts',
'always-warn reply',
'warn-posts',
'warn reply',
anchorPost,
),
)
@@ -288,6 +423,88 @@ async function main() {
'account reply',
anchorPost,
)
await server.mocker.createPost('blocking-account', 'blocking post')
await server.mocker.createQuotePost(
'blocking-account',
'blocking quote post',
anchorPost,
)
await server.mocker.createReply(
'blocking-account',
'blocking reply',
anchorPost,
)
await server.mocker.users.alice.agent.app.bsky.graph.block.create(
{
repo: server.mocker.users.alice.did,
},
{
subject: server.mocker.users['blocking-account'].did,
createdAt: new Date().toISOString(),
},
)
await server.mocker.createPost('blockedby-account', 'blockedby post')
await server.mocker.createQuotePost(
'blockedby-account',
'blockedby quote post',
anchorPost,
)
await server.mocker.createReply(
'blockedby-account',
'blockedby reply',
anchorPost,
)
await server.mocker.users[
'blockedby-account'
].agent.app.bsky.graph.block.create(
{
repo: server.mocker.users['blockedby-account'].did,
},
{
subject: server.mocker.users.alice.did,
createdAt: new Date().toISOString(),
},
)
await server.mocker.createPost(
'mutual-block-account',
'mutual-block post',
)
await server.mocker.createQuotePost(
'mutual-block-account',
'mutual-block quote post',
anchorPost,
)
await server.mocker.createReply(
'mutual-block-account',
'mutual-block reply',
anchorPost,
)
await server.mocker.users.alice.agent.app.bsky.graph.block.create(
{
repo: server.mocker.users.alice.did,
},
{
subject: server.mocker.users['mutual-block-account'].did,
createdAt: new Date().toISOString(),
},
)
await server.mocker.users[
'mutual-block-account'
].agent.app.bsky.graph.block.create(
{
repo: server.mocker.users['mutual-block-account'].did,
},
{
subject: server.mocker.users.alice.did,
createdAt: new Date().toISOString(),
},
)
// flush caches
await server.mocker.testNet.processAll()
}
}
console.log('Ready')
+3 -4
View File
@@ -1,18 +1,17 @@
/* eslint-env detox/detox */
import {openApp, login, createServer, sleep} from '../util'
import {openApp, loginAsAlice, createServer, sleep} from '../util'
describe('Composer', () => {
let service: string
beforeAll(async () => {
service = await createServer('?users')
await createServer('?users')
await openApp({
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
})
})
it('Login', async () => {
await login(service, 'alice', 'hunter2')
await loginAsAlice()
await element(by.id('homeScreenFeedTabs-Following')).tap()
})
+6
View File
@@ -25,6 +25,12 @@ describe('Create account', () => {
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()
await element(by.id('continueBtn')).tap()
await expect(element(by.id('recommendedFollowsOnboarding'))).toBeVisible()
await element(by.id('continueBtn')).tap()
await expect(element(by.id('homeScreen'))).toBeVisible()
})
})
+8 -9
View File
@@ -1,16 +1,15 @@
/* eslint-env detox/detox */
import {openApp, login, createServer} from '../util'
import {openApp, loginAsAlice, createServer} from '../util'
describe('Home screen', () => {
let service: string
beforeAll(async () => {
service = await createServer('?users&follows&posts')
await createServer('?users&follows&posts')
await openApp({permissions: {notifications: 'YES'}})
})
it('Login', async () => {
await login(service, 'alice', 'hunter2')
await loginAsAlice()
await element(by.id('homeScreenFeedTabs-Following')).tap()
})
@@ -55,13 +54,13 @@ describe('Home screen', () => {
await element(by.id('postDropdownBtn').withAncestor(carlaPosts))
.atIndex(0)
.tap()
await element(by.id('postDropdownReportBtn')).tap()
await expect(element(by.id('reportPostModal'))).toBeVisible()
await element(by.text('Report post')).tap()
await expect(element(by.id('reportModal'))).toBeVisible()
await element(
by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportPostModal'))).not.toBeVisible()
await expect(element(by.id('reportModal'))).not.toBeVisible()
})
it('Can swipe between feeds', async () => {
@@ -84,7 +83,7 @@ describe('Home screen', () => {
await element(by.id('postDropdownBtn').withAncestor(alicePosts))
.atIndex(0)
.tap()
await element(by.id('postDropdownDeleteBtn')).tap()
await element(by.text('Delete post')).tap()
await expect(element(by.id('confirmModal'))).toBeVisible()
await element(by.id('confirmBtn')).tap()
await expect(
+21 -15
View File
@@ -1,6 +1,11 @@
/* eslint-env detox/detox */
import {openApp, login, createServer} from '../util'
/**
* This test is being skipped until we can resolve the detox crash issue
* with the side drawer.
*/
import {openApp, loginAsAlice, createServer} from '../util'
describe('invite-codes', () => {
let service: string
@@ -12,17 +17,13 @@ describe('invite-codes', () => {
it('I can fetch invite codes', async () => {
await expect(element(by.id('signInButton'))).toBeVisible()
await login(service, 'alice', 'hunter2')
await element(by.id('viewHeaderDrawerBtn')).tap()
await expect(element(by.id('drawer'))).toBeVisible()
await element(by.id('menuItemInviteCodes')).tap()
await loginAsAlice()
await element(by.id('e2eOpenInviteCodesModal')).tap()
await expect(element(by.id('inviteCodesModal'))).toBeVisible()
const attrs = await element(by.id('inviteCode-0-code')).getAttributes()
inviteCode = attrs.text
await element(by.id('closeBtn')).tap()
await element(by.id('viewHeaderDrawerBtn')).tap()
await element(by.id('menuItemButton-Settings')).tap()
await element(by.id('signOutBtn')).tap()
await element(by.id('e2eSignOut')).tap()
})
it('I can create a new account with the invite code', async () => {
@@ -42,17 +43,22 @@ describe('invite-codes', () => {
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()
await element(by.id('continueBtn')).tap()
await expect(element(by.id('recommendedFollowsOnboarding'))).toBeVisible()
await element(by.id('continueBtn')).tap()
await expect(element(by.id('homeScreen'))).toBeVisible()
await element(by.id('viewHeaderDrawerBtn')).tap()
await element(by.id('menuItemButton-Settings')).tap()
await element(by.id('signOutBtn')).tap()
})
it('I get a notification for the new user', async () => {
await expect(element(by.id('signInButton'))).toBeVisible()
await login(service, 'alice', 'hunter2')
await element(by.id('viewHeaderDrawerBtn')).tap()
await element(by.id('menuItemButton-Notifications')).tap()
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()
})
+157
View File
@@ -0,0 +1,157 @@
/* eslint-env detox/detox */
import {openApp, loginAsAlice, createServer} from '../util'
describe('Mergefeed', () => {
beforeAll(async () => {
await createServer('?mergefeed')
await openApp({permissions: {notifications: 'YES'}})
})
it('Login', async () => {
await loginAsAlice()
await element(by.id('e2eToggleMergefeed')).tap()
})
it('Sees the expected mix of posts with default filters', async () => {
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
'down',
'slow',
1,
0.5,
0.5,
)
// followed users
await expect(
element(
by.id('postText').withAncestor(by.id('feedItem-by-carla.test')),
).atIndex(0),
).toHaveText('Post 9')
await expect(
element(
by.id('postText').withAncestor(by.id('feedItem-by-bob.test')),
).atIndex(0),
).toHaveText('Post 9')
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
'up',
'fast',
1,
0.5,
0.5,
)
// feed users
await expect(
element(
by.id('postText').withAncestor(by.id('feedItem-by-dan.test')),
).atIndex(0),
).toHaveText('Post 0')
})
it('Sees the expected mix of posts with replies disabled', async () => {
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
'down',
'fast',
1,
0.5,
0.5,
)
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
'down',
'fast',
1,
0.5,
0.5,
)
await element(by.id('viewHeaderHomeFeedPrefsBtn')).tap()
await element(by.id('toggleRepliesBtn')).tap()
await element(by.id('confirmBtn')).tap()
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
'down',
'slow',
1,
0.5,
0.5,
)
// followed users
await expect(
element(
by.id('postText').withAncestor(by.id('feedItem-by-carla.test')),
).atIndex(0),
).toHaveText('Post 9')
await expect(
element(
by.id('postText').withAncestor(by.id('feedItem-by-bob.test')),
).atIndex(0),
).toHaveText('Post 9')
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
'up',
'fast',
1,
0.5,
0.5,
)
// feed users
await expect(
element(
by.id('postText').withAncestor(by.id('feedItem-by-dan.test')),
).atIndex(0),
).toHaveText('Post 0')
})
it('Sees the expected mix of posts with no follows', async () => {
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
'down',
'fast',
1,
0.5,
0.5,
)
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('unfollowBtn')).tap()
await element(by.id('profileHeaderBackBtn')).tap()
// have to wait for the toast to clear
await waitFor(element(by.id('searchTextInputClearBtn')))
.toBeVisible()
.withTimeout(5000)
await element(by.id('searchTextInputClearBtn')).tap()
await element(by.id('searchTextInput')).typeText('carla')
await element(by.id('searchAutoCompleteResult-carla.test')).tap()
await expect(element(by.id('profileView'))).toBeVisible()
await element(by.id('unfollowBtn')).tap()
await element(by.id('profileHeaderBackBtn')).tap()
await element(by.id('bottomBarHomeBtn')).tap()
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
'down',
'slow',
1,
0.5,
0.5,
)
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
'down',
'slow',
1,
0.5,
0.5,
)
// followed users NOT present
await expect(element(by.id('feedItem-by-carla.test'))).not.toExist()
await expect(element(by.id('feedItem-by-bob.test'))).not.toExist()
// feed users
await expect(
element(
by.id('postText').withAncestor(by.id('feedItem-by-dan.test')),
).atIndex(0),
).toHaveText('Post 0')
})
})
+31 -13
View File
@@ -1,11 +1,10 @@
/* eslint-env detox/detox */
import {openApp, login, createServer, sleep} from '../util'
import {openApp, loginAsAlice, loginAsBob, createServer, sleep} from '../util'
describe('Profile screen', () => {
let service: string
describe('Mute lists', () => {
beforeAll(async () => {
service = await createServer('?users&follows&labels')
await createServer('?users&follows&labels')
await openApp({
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
})
@@ -13,10 +12,8 @@ describe('Profile screen', () => {
it('Login and view my mutelists', async () => {
await expect(element(by.id('signInButton'))).toBeVisible()
await login(service, 'alice', 'hunter2')
await element(by.id('viewHeaderDrawerBtn')).tap()
await expect(element(by.id('drawer'))).toBeVisible()
await element(by.id('menuItemButton-Moderation')).tap()
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()
@@ -65,7 +62,7 @@ describe('Profile screen', () => {
await element(by.id('editListBtn')).tap()
await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible()
await element(by.id('changeAvatarBtn')).tap()
await element(by.id('changeAvatarLibraryBtn')).tap()
await element(by.text('Library')).tap()
await sleep(3e3)
await element(by.id('saveBtn')).tap()
await expect(element(by.id('createOrEditMuteListModal'))).not.toBeVisible()
@@ -81,7 +78,7 @@ describe('Profile screen', () => {
await element(by.id('editListBtn')).tap()
await expect(element(by.id('createOrEditMuteListModal'))).toBeVisible()
await element(by.id('changeAvatarBtn')).tap()
await element(by.id('changeAvatarRemoveBtn')).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()
@@ -114,7 +111,8 @@ describe('Profile screen', () => {
it('Shows the mutelist on my profile', async () => {
await element(by.id('bottomBarProfileBtn')).tap()
await element(by.id('selector-2')).tap()
await element(by.id('selector')).swipe('left')
await element(by.id('selector-4')).tap()
await element(by.id('list-Bad Ppl')).tap()
})
@@ -125,17 +123,37 @@ describe('Profile screen', () => {
await expect(element(by.id('profileView'))).toBeVisible()
await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.id('profileHeaderDropdownListAddRemoveBtn')).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.id('profileHeaderDropdownListAddRemoveBtn')).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()
})
})
+29 -22
View File
@@ -1,11 +1,10 @@
/* eslint-env detox/detox */
import {openApp, login, createServer, sleep} from '../util'
import {openApp, loginAsAlice, createServer, sleep} from '../util'
describe('Profile screen', () => {
let service: string
beforeAll(async () => {
service = await createServer('?users&posts')
await createServer('?users&posts&feeds')
await openApp({
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
})
@@ -13,10 +12,18 @@ describe('Profile screen', () => {
it('Login and navigate to my profile', async () => {
await expect(element(by.id('signInButton'))).toBeVisible()
await login(service, 'alice', 'hunter2')
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 expect(element(by.id('feed-alice-favs'))).toBeVisible()
await element(by.id('selector')).swipe('right')
await element(by.id('selector-0')).tap()
})
it('Open and close edit profile modal', async () => {
await element(by.id('profileHeaderEditProfileButton')).tap()
await expect(element(by.id('editProfileModal'))).toBeVisible()
@@ -53,7 +60,7 @@ describe('Profile screen', () => {
await expect(element(by.id('profileHeaderDisplayName'))).toHaveText(
'alice.test',
)
await expect(element(by.id('profileHeaderDescription'))).toHaveText('')
await expect(element(by.id('profileHeaderDescription'))).not.toExist()
})
it('Set avi and banner via the edit profile modal', async () => {
@@ -62,10 +69,10 @@ describe('Profile screen', () => {
await element(by.id('profileHeaderEditProfileButton')).tap()
await expect(element(by.id('editProfileModal'))).toBeVisible()
await element(by.id('changeBannerBtn')).tap()
await element(by.id('changeBannerLibraryBtn')).tap()
await element(by.text('Library')).tap()
await sleep(3e3)
await element(by.id('changeAvatarBtn')).tap()
await element(by.id('changeAvatarLibraryBtn')).tap()
await element(by.text('Library')).tap()
await sleep(3e3)
await element(by.id('editProfileSaveBtn')).tap()
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
@@ -79,9 +86,9 @@ describe('Profile screen', () => {
await element(by.id('profileHeaderEditProfileButton')).tap()
await expect(element(by.id('editProfileModal'))).toBeVisible()
await element(by.id('changeBannerBtn')).tap()
await element(by.id('changeBannerRemoveBtn')).tap()
await element(by.text('Remove')).tap()
await element(by.id('changeAvatarBtn')).tap()
await element(by.id('changeAvatarRemoveBtn')).tap()
await element(by.text('Remove')).tap()
await element(by.id('editProfileSaveBtn')).tap()
await expect(element(by.id('editProfileModal'))).not.toBeVisible()
await expect(element(by.id('userBannerFallback'))).toExist()
@@ -107,24 +114,24 @@ describe('Profile screen', () => {
})
it('Can mute/unmute another user', async () => {
await expect(element(by.id('profileHeaderMutedNotice'))).not.toExist()
await expect(element(by.id('profileHeaderAlert'))).not.toExist()
await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.id('profileHeaderDropdownMuteBtn')).tap()
await expect(element(by.id('profileHeaderMutedNotice'))).toBeVisible()
await element(by.text('Mute Account')).tap()
await expect(element(by.id('profileHeaderAlert'))).toBeVisible()
await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.id('profileHeaderDropdownMuteBtn')).tap()
await expect(element(by.id('profileHeaderMutedNotice'))).not.toExist()
await element(by.text('Unmute Account')).tap()
await expect(element(by.id('profileHeaderAlert'))).not.toExist()
})
it('Can report another user', async () => {
await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.id('profileHeaderDropdownReportBtn')).tap()
await expect(element(by.id('reportAccountModal'))).toBeVisible()
await element(by.text('Report Account')).tap()
await expect(element(by.id('reportModal'))).toBeVisible()
await element(
by.id('reportAccountRadios-com.atproto.moderation.defs#reasonSpam'),
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportAccountModal'))).not.toBeVisible()
await expect(element(by.id('reportModal'))).not.toBeVisible()
})
it('Can like posts', async () => {
@@ -166,12 +173,12 @@ describe('Profile screen', () => {
it('Can report posts', async () => {
const posts = by.id('feedItem-by-bob.test')
await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap()
await element(by.id('postDropdownReportBtn')).tap()
await expect(element(by.id('reportPostModal'))).toBeVisible()
await element(by.text('Report post')).tap()
await expect(element(by.id('reportModal'))).toBeVisible()
await element(
by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportPostModal'))).not.toBeVisible()
await expect(element(by.id('reportModal'))).not.toBeVisible()
})
})
+3 -4
View File
@@ -1,18 +1,17 @@
/* eslint-env detox/detox */
import {openApp, login, createServer} from '../util'
import {openApp, loginAsAlice, createServer} from '../util'
describe('Search screen', () => {
let service: string
beforeAll(async () => {
service = await createServer('?users')
await createServer('?users')
await openApp({
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
})
})
it('Login', async () => {
await login(service, 'alice', 'hunter2')
await loginAsAlice()
})
it('Navigate to another user profile via autocomplete', async () => {
+33
View File
@@ -0,0 +1,33 @@
/* eslint-env detox/detox */
import {openApp, loginAsAlice, createServer, sleep} from '../util'
describe('Self-labeling', () => {
beforeAll(async () => {
await createServer('?users')
await openApp({
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
})
})
it('Login', async () => {
await loginAsAlice()
await element(by.id('homeScreenFeedTabs-Following')).tap()
})
it('Post an image with the porn label', async () => {
await element(by.id('composeFAB')).tap()
await element(by.id('composerTextInput')).typeText('Post with an image')
await element(by.id('openGalleryBtn')).tap()
await sleep(3e3)
await element(by.id('labelsBtn')).tap()
await element(by.id('pornLabelBtn')).tap()
await element(by.id('confirmBtn')).tap()
await element(by.id('composerPublishBtn')).tap()
await expect(element(by.id('composeFAB'))).toBeVisible()
const posts = by.id('feedItem-by-alice.test')
await expect(
element(by.id('contentHider-embed').withAncestor(posts)).atIndex(0),
).toExist()
})
})
@@ -1,16 +1,15 @@
/* eslint-env detox/detox */
import {openApp, login, createServer} from '../util'
import {openApp, loginAsAlice, createServer} from '../util'
describe('Shell', () => {
let service: string
beforeAll(async () => {
service = await createServer('?users')
await createServer('?users')
await openApp({permissions: {notifications: 'YES'}})
})
it('Login', async () => {
await login(service, 'alice', 'hunter2')
await loginAsAlice()
await element(by.id('homeScreenFeedTabs-Following')).tap()
})
+9 -24
View File
@@ -1,42 +1,34 @@
/* eslint-env detox/detox */
import {openApp, login, createServer} from '../util'
import {openApp, loginAsAlice, loginAsBob, createServer} from '../util'
describe('Thread muting', () => {
let service: string
beforeAll(async () => {
service = await createServer('?users&follows')
await createServer('?users&follows')
await openApp({permissions: {notifications: 'YES'}})
})
it('Login, create a thread, and log out', async () => {
await login(service, 'alice', 'hunter2')
await loginAsAlice()
await element(by.id('homeScreenFeedTabs-Following')).tap()
await element(by.id('composeFAB')).tap()
await element(by.id('composerTextInput')).typeText('Test thread')
await element(by.id('composerPublishBtn')).tap()
await expect(element(by.id('composeFAB'))).toBeVisible()
await element(by.id('viewHeaderDrawerBtn')).tap()
await element(by.id('menuItemButton-Settings')).tap()
await element(by.id('signOutBtn')).tap()
})
it('Login, reply to the thread, and log out', async () => {
await login(service, 'bob', 'hunter2')
await loginAsBob()
await element(by.id('homeScreenFeedTabs-Following')).tap()
const alicePosts = by.id('feedItem-by-alice.test')
await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap()
await element(by.id('composerTextInput')).typeText('Reply 1')
await element(by.id('composerPublishBtn')).tap()
await expect(element(by.id('composeFAB'))).toBeVisible()
await element(by.id('viewHeaderDrawerBtn')).tap()
await element(by.id('menuItemButton-Settings')).tap()
await element(by.id('signOutBtn')).tap()
})
it('Login, confirm notification exists, mute thread, and log out', async () => {
await login(service, 'alice', 'hunter2')
await loginAsAlice()
await element(by.id('bottomBarNotificationsBtn')).tap()
const bobNotifs = by.id('feedItem-by-bob.test')
await expect(
@@ -45,19 +37,15 @@ describe('Thread muting', () => {
await element(by.id('postDropdownBtn').withAncestor(bobNotifs))
.atIndex(0)
.tap()
await element(by.id('postDropdownMuteThreadBtn')).tap()
await element(by.text('Mute thread')).tap()
// have to wait for the toast to clear
await waitFor(element(by.id('viewHeaderDrawerBtn')))
.toBeVisible()
.withTimeout(5000)
await element(by.id('viewHeaderDrawerBtn')).tap()
await element(by.id('menuItemButton-Settings')).tap()
await element(by.id('signOutBtn')).tap()
})
it('Login, reply to the thread twice, and log out', async () => {
await login(service, 'bob', 'hunter2')
await loginAsBob()
await element(by.id('bottomBarProfileBtn')).tap()
await element(by.id('selector-1')).tap()
@@ -74,13 +62,10 @@ describe('Thread muting', () => {
await expect(element(by.id('composeFAB'))).toBeVisible()
await element(by.id('bottomBarHomeBtn')).tap()
await element(by.id('viewHeaderDrawerBtn')).tap()
await element(by.id('menuItemButton-Settings')).tap()
await element(by.id('signOutBtn')).tap()
})
it('Login, confirm notifications dont exist, unmute the thread, confirm notifications exist', async () => {
await login(service, 'alice', 'hunter2')
await loginAsAlice()
await element(by.id('bottomBarNotificationsBtn')).tap()
const bobNotifs = by.id('feedItem-by-bob.test')
@@ -93,7 +78,7 @@ describe('Thread muting', () => {
await element(by.id('postDropdownBtn').withAncestor(alicePosts))
.atIndex(0)
.tap()
await element(by.id('postDropdownMuteThreadBtn')).tap()
await element(by.text('Unmute thread')).tap()
// TODO
// the swipe down to trigger PTR isnt working and I dont want to block on this
+11 -12
View File
@@ -1,16 +1,15 @@
/* eslint-env detox/detox */
import {openApp, login, createServer} from '../util'
import {openApp, loginAsAlice, createServer} from '../util'
describe('Thread screen', () => {
let service: string
beforeAll(async () => {
service = await createServer('?users&follows&thread')
await createServer('?users&follows&thread')
await openApp({permissions: {notifications: 'YES'}})
})
it('Login & navigate to thread', async () => {
await login(service, 'alice', 'hunter2')
await loginAsAlice()
await element(by.id('homeScreenFeedTabs-Following')).tap()
await element(by.id('feedItem-by-bob.test')).atIndex(0).tap()
await expect(
@@ -104,24 +103,24 @@ describe('Thread screen', () => {
it('Can report the root post', async () => {
const post = by.id('postThreadItem-by-bob.test')
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
await element(by.id('postDropdownReportBtn')).tap()
await expect(element(by.id('reportPostModal'))).toBeVisible()
await element(by.text('Report post')).tap()
await expect(element(by.id('reportModal'))).toBeVisible()
await element(
by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportPostModal'))).not.toBeVisible()
await expect(element(by.id('reportModal'))).not.toBeVisible()
})
it('Can report a reply post', async () => {
const post = by.id('postThreadItem-by-carla.test')
await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap()
await element(by.id('postDropdownReportBtn')).tap()
await expect(element(by.id('reportPostModal'))).toBeVisible()
await element(by.text('Report post')).tap()
await expect(element(by.id('reportModal'))).toBeVisible()
await element(
by.id('reportPostRadios-com.atproto.moderation.defs#reasonSpam'),
by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'),
).tap()
await element(by.id('sendReportBtn')).tap()
await expect(element(by.id('reportPostModal'))).not.toBeVisible()
await expect(element(by.id('reportModal'))).not.toBeVisible()
})
})
+22
View File
@@ -1,10 +1,24 @@
import {resolveConfig} from 'detox/internals'
import {execSync} from 'child_process'
const platform = device.getPlatform()
export async function openApp(opts: any) {
opts = opts || {}
const config = await resolveConfig()
if (device.getPlatform() === 'ios') {
// disable password autofill
execSync(
`plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles/UserSettings.plist`,
)
execSync(
`plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/EffectiveUserSettings.plist`,
)
execSync(
`plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/PublicInfo/PublicEffectiveUserSettings.plist`,
)
}
if (config.configurationName.split('.').includes('debug')) {
return await openAppForDebugBuild(platform, opts)
} else {
@@ -55,6 +69,14 @@ export async function login(
await element(by.id('loginNextButton')).tap()
}
export async function loginAsAlice() {
await element(by.id('e2eSignInAlice')).tap()
}
export async function loginAsBob() {
await element(by.id('e2eSignInBob')).tap()
}
async function openAppForDebugBuild(platform: string, opts: any) {
const deepLinkUrl = // Local testing with packager
/*process.env.EXPO_USE_UPDATES
+5 -2
View File
@@ -1,4 +1,4 @@
import React from 'react'
import React, {ReactNode} from 'react'
import {View, ScrollView, Modal, FlatList, TextInput} from 'react-native'
const BottomSheetModalContext = React.createContext(null)
@@ -6,7 +6,10 @@ const BottomSheetModalContext = React.createContext(null)
const BottomSheetModalProvider = (props: any) => {
return <BottomSheetModalContext.Provider {...props} value={{}} />
}
class BottomSheet extends React.Component<{onClose?: () => void}> {
class BottomSheet extends React.Component<{
onClose?: () => void
children?: ReactNode
}> {
snapToIndex() {}
snapToPosition() {}
expand() {}
+1
View File
@@ -0,0 +1 @@
export const Slider = {}
+10
View File
@@ -0,0 +1,10 @@
jest.mock('sentry-expo', () => ({
init: () => jest.fn(),
Native: {
ReactNativeTracing: jest.fn().mockImplementation(() => ({
start: jest.fn(),
stop: jest.fn(),
})),
ReactNavigationInstrumentation: jest.fn(),
},
}))
+2
View File
@@ -0,0 +1,2 @@
export const DropdownMenu = jest.fn().mockImplementation(() => {})
export const create = jest.fn().mockImplementation(() => {})
+1 -103
View File
@@ -1,106 +1,4 @@
import {
LikelyType,
getLinkMeta,
getLikelyType,
} from '../../src/lib/link-meta/link-meta'
import {exampleComHtml} from './__mocks__/exampleComHtml'
import {BskyAgent} from '@atproto/api'
import {DEFAULT_SERVICE, RootStoreModel} from '../../src/state'
describe('getLinkMeta', () => {
let rootStore: RootStoreModel
beforeEach(() => {
rootStore = new RootStoreModel(new BskyAgent({service: DEFAULT_SERVICE}))
})
const inputs = [
'',
'httpbadurl',
'https://example.com',
'https://example.com/index.html',
'https://example.com/image.png',
'https://example.com/video.avi',
'https://example.com/audio.ogg',
'https://example.com/text.txt',
'https://example.com/javascript.js',
'https://bsky.app/',
'https://bsky.app/index.html',
]
const outputs = [
{
error: 'Invalid URL',
likelyType: LikelyType.Other,
url: '',
},
{
error: 'Invalid URL',
likelyType: LikelyType.Other,
url: 'httpbadurl',
},
{
likelyType: LikelyType.HTML,
url: 'https://example.com',
title: 'Example Domain',
description: 'An example website',
},
{
likelyType: LikelyType.HTML,
url: 'https://example.com/index.html',
title: 'Example Domain',
description: 'An example website',
},
{
likelyType: LikelyType.Image,
url: 'https://example.com/image.png',
},
{
likelyType: LikelyType.Video,
url: 'https://example.com/video.avi',
},
{
likelyType: LikelyType.Audio,
url: 'https://example.com/audio.ogg',
},
{
likelyType: LikelyType.Text,
url: 'https://example.com/text.txt',
},
{
likelyType: LikelyType.Other,
url: 'https://example.com/javascript.js',
},
{
likelyType: LikelyType.AtpData,
url: '/',
},
{
likelyType: LikelyType.AtpData,
url: '/index.html',
},
{
likelyType: LikelyType.Other,
url: '',
title: '',
},
]
it('correctly handles a set of text inputs', async () => {
for (let i = 0; i < inputs.length; i++) {
global.fetch = jest.fn().mockImplementationOnce(() => {
return new Promise((resolve, _reject) => {
resolve({
ok: true,
status: 200,
text: () => exampleComHtml,
})
})
})
const input = inputs[i]
const output = await getLinkMeta(rootStore, input)
expect(output).toEqual(outputs[i])
}
})
})
import {LikelyType, getLikelyType} from '../../src/lib/link-meta/link-meta'
describe('getLikelyType', () => {
it('correctly handles non-parsed url', async () => {
+62 -4
View File
@@ -1,3 +1,4 @@
import {RichText} from '@atproto/api'
import {
getYoutubeVideoId,
makeRecordUri,
@@ -8,6 +9,7 @@ import {
import {pluralize, enforceLen} from '../../src/lib/strings/helpers'
import {ago} from '../../src/lib/strings/time'
import {detectLinkables} from '../../src/lib/strings/rich-text-detection'
import {shortenLinks} from '../../src/lib/strings/rich-text-manip'
import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles'
import {cleanError} from '../../src/lib/strings/errors'
@@ -48,7 +50,7 @@ describe('detectLinkables', () => {
'Classic article https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/ ',
'https://foo.com https://bar.com/whatever https://baz.com',
'punctuation https://foo.com, https://bar.com/whatever; https://baz.com.',
'parenthentical (https://foo.com)',
'parenthetical (https://foo.com)',
'except for https://foo.com/thing_(cool)',
]
const outputs = [
@@ -112,7 +114,7 @@ describe('detectLinkables', () => {
{link: 'https://baz.com'},
'.',
],
['parenthentical (', {link: 'https://foo.com'}, ')'],
['parenthetical (', {link: 'https://foo.com'}, ')'],
['except for ', {link: 'https://foo.com/thing_(cool)'}],
]
it('correctly handles a set of text inputs', () => {
@@ -173,6 +175,7 @@ describe('ago', () => {
1671461038,
'04 Dec 1995 00:12:00 GMT',
new Date(),
new Date().setSeconds(new Date().getSeconds() - 10),
new Date().setMinutes(new Date().getMinutes() - 10),
new Date().setHours(new Date().getHours() - 1),
new Date().setDate(new Date().getDate() - 1),
@@ -181,7 +184,8 @@ describe('ago', () => {
const outputs = [
new Date(1671461038).toLocaleDateString(),
new Date('04 Dec 1995 00:12:00 GMT').toLocaleDateString(),
'0s',
'now',
'10s',
'10m',
'1h',
'1d',
@@ -296,11 +300,15 @@ describe('toShortUrl', () => {
'https://bsky.app',
'https://bsky.app/3jk7x4irgv52r',
'https://bsky.app/3jk7x4irgv52r2313y182h9',
'https://very-long-domain-name.com/foo',
'https://very-long-domain-name.com/foo?bar=baz#andsomemore',
]
const outputs = [
'bsky.app',
'bsky.app/3jk7x4irgv52r',
'bsky.app/3jk7x4irgv52r2313y...',
'bsky.app/3jk7x4irgv52...',
'very-long-domain-name.com/foo',
'very-long-domain-name.com/foo?bar=baz#...',
]
it('shortens the url', () => {
@@ -352,3 +360,53 @@ describe('getYoutubeVideoId', () => {
expect(getYoutubeVideoId('https://youtu.be/videoId')).toBe('videoId')
})
})
describe('shortenLinks', () => {
const inputs = [
'start https://middle.com/foo/bar?baz=bux#hash end',
'https://start.com/foo/bar?baz=bux#hash middle end',
'start middle https://end.com/foo/bar?baz=bux#hash',
'https://newline1.com/very/long/url/here\nhttps://newline2.com/very/long/url/here',
'Classic article https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',
]
const outputs = [
[
'start middle.com/foo/bar?baz=... end',
['https://middle.com/foo/bar?baz=bux#hash'],
],
[
'start.com/foo/bar?baz=... middle end',
['https://start.com/foo/bar?baz=bux#hash'],
],
[
'start middle end.com/foo/bar?baz=...',
['https://end.com/foo/bar?baz=bux#hash'],
],
[
'newline1.com/very/long/ur...\nnewline2.com/very/long/ur...',
[
'https://newline1.com/very/long/url/here',
'https://newline2.com/very/long/url/here',
],
],
[
'Classic article socket3.wordpress.com/2018/02/03/d...',
[
'https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',
],
],
]
it('correctly shortens rich text while preserving facet URIs', () => {
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i]
const inputRT = new RichText({text: input})
inputRT.detectFacetsWithoutResolution()
const outputRT = shortenLinks(inputRT)
expect(outputRT.text).toEqual(outputs[i][0])
expect(outputRT.facets?.length).toEqual(outputs[i][1].length)
for (let j = 0; j < outputs[i][1].length; j++) {
expect(outputRT.facets![j].features[0].uri).toEqual(outputs[i][1][j])
}
}
})
})
+134
View File
@@ -0,0 +1,134 @@
import {
linkRequiresWarning,
isPossiblyAUrl,
splitApexDomain,
} from '../../../src/lib/strings/url-helpers'
describe('linkRequiresWarning', () => {
type Case = [string, string, boolean]
const cases: Case[] = [
['http://example.com', 'http://example.com', false],
['http://example.com', 'example.com', false],
['http://example.com', 'example.com/page', false],
['http://example.com', '', true],
['http://example.com', 'other.com', true],
['http://example.com', 'http://other.com', true],
['http://example.com', 'some label', true],
['http://example.com', 'example.com more', true],
['http://example.com', 'http://example.co', true],
['http://example.co', 'http://example.com', true],
['http://example.com', 'example.co', true],
['http://example.co', 'example.com', true],
['http://site.pages.dev', 'http://site.page', true],
['http://site.page', 'http://site.pages.dev', true],
['http://site.pages.dev', 'site.page', true],
['http://site.page', 'site.pages.dev', true],
['http://site.pages.dev', 'http://site.pages', true],
['http://site.pages', 'http://site.pages.dev', true],
['http://site.pages.dev', 'site.pages', true],
['http://site.pages', 'site.pages.dev', true],
['http://bsky.app/profile/bob.test/post/3kbeuduu7m22v', 'my post', false],
['https://bsky.app/profile/bob.test/post/3kbeuduu7m22v', 'my post', false],
['http://bsky.app/', 'bluesky', false],
['https://bsky.app/', 'bluesky', false],
[
'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v',
'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v',
false,
],
[
'https://bsky.app/profile/bob.test/post/3kbeuduu7m22v',
'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v',
false,
],
[
'http://bsky.app/',
'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v',
false,
],
[
'https://bsky.app/',
'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v',
false,
],
[
'http://bsky.app/profile/bob.test/post/3kbeuduu7m22v',
'https://google.com',
true,
],
[
'https://bsky.app/profile/bob.test/post/3kbeuduu7m22v',
'https://google.com',
true,
],
['http://bsky.app/', 'https://google.com', true],
['https://bsky.app/', 'https://google.com', true],
// bad uri inputs, default to true
['', '', true],
['example.com', 'example.com', true],
]
it.each(cases)(
'given input uri %p and text %p, returns %p',
(uri, text, expected) => {
const output = linkRequiresWarning(uri, text)
expect(output).toEqual(expected)
},
)
})
describe('isPossiblyAUrl', () => {
type Case = [string, boolean]
const cases: Case[] = [
['', false],
['text', false],
['some text', false],
['some text', false],
['some domain.com', false],
['domain.com', true],
[' domain.com', true],
['domain.com ', true],
[' domain.com ', true],
['http://domain.com', true],
[' http://domain.com', true],
['http://domain.com ', true],
[' http://domain.com ', true],
['https://domain.com', true],
[' https://domain.com', true],
['https://domain.com ', true],
[' https://domain.com ', true],
['http://domain.com/foo', true],
['http://domain.com stuff', true],
]
it.each(cases)('given input uri %p, returns %p', (str, expected) => {
const output = isPossiblyAUrl(str)
expect(output).toEqual(expected)
})
})
describe('splitApexDomain', () => {
type Case = [string, string, string]
const cases: Case[] = [
['', '', ''],
['example.com', '', 'example.com'],
['foo.example.com', 'foo.', 'example.com'],
['foo.bar.example.com', 'foo.bar.', 'example.com'],
['example.co.uk', '', 'example.co.uk'],
['foo.example.co.uk', 'foo.', 'example.co.uk'],
['example.nonsense', '', 'example.nonsense'],
['foo.example.nonsense', '', 'foo.example.nonsense'],
['foo.bar.example.nonsense', '', 'foo.bar.example.nonsense'],
['example.com.example.com', 'example.com.', 'example.com'],
]
it.each(cases)(
'given input uri %p, returns %p,%p',
(str, expected1, expected2) => {
const output = splitApexDomain(str)
expect(output[0]).toEqual(expected1)
expect(output[1]).toEqual(expected2)
},
)
})
+114
View File
@@ -0,0 +1,114 @@
module.exports = function () {
const hasSentryToken = !!process.env.SENTRY_AUTH_TOKEN
return {
expo: {
name: 'Bluesky',
slug: 'bluesky',
scheme: 'bluesky',
owner: 'blueskysocial',
version: '1.55.0',
runtimeVersion: {
policy: 'appVersion',
},
orientation: 'portrait',
icon: './assets/icon.png',
userInterfaceStyle: 'automatic',
splash: {
image: './assets/cloud-splash.png',
resizeMode: 'cover',
backgroundColor: '#ffffff',
},
ios: {
buildNumber: '1',
supportsTablet: false,
bundleIdentifier: 'xyz.blueskyweb.app',
config: {
usesNonExemptEncryption: false,
},
infoPlist: {
UIBackgroundModes: ['remote-notification'],
NSCameraUsageDescription:
'Used for profile pictures, posts, and other kinds of content.',
NSMicrophoneUsageDescription:
'Used for posts and other kinds of content.',
NSPhotoLibraryAddUsageDescription:
'Used to save images to your library.',
NSPhotoLibraryUsageDescription:
'Used for profile pictures, posts, and other kinds of content',
},
associatedDomains: ['applinks:bsky.app', 'applinks:staging.bsky.app'],
},
androidStatusBar: {
barStyle: 'dark-content',
backgroundColor: '#ffffff',
},
android: {
versionCode: 44,
adaptiveIcon: {
foregroundImage: './assets/adaptive-icon.png',
backgroundColor: '#ffffff',
},
googleServicesFile: './google-services.json',
package: 'xyz.blueskyweb.app',
intentFilters: [
{
action: 'VIEW',
autoVerify: true,
data: [
{
scheme: 'https',
host: 'bsky.app',
},
],
category: ['BROWSABLE', 'DEFAULT'],
},
],
},
web: {
favicon: './assets/favicon.png',
},
updates: {
enabled: true,
fallbackToCacheTimeout: 1000,
url: 'https://u.expo.dev/55bd077a-d905-4184-9c7f-94789ba0f302',
},
plugins: [
'expo-localization',
hasSentryToken && 'sentry-expo',
[
'expo-build-properties',
{
android: {
compileSdkVersion: 34,
targetSdkVersion: 34,
buildToolsVersion: '34.0.0',
kotlinVersion: '1.8.0',
},
},
],
[
'expo-updates',
{
username: 'blueskysocial',
},
],
].filter(Boolean),
extra: {
eas: {
projectId: '55bd077a-d905-4184-9c7f-94789ba0f302',
},
},
hooks: {
postPublish: [
{
file: 'sentry-expo/upload-sourcemaps',
config: {
organization: 'blueskyweb',
project: 'react-native',
},
},
],
},
},
}
}
-102
View File
@@ -1,102 +0,0 @@
{
"expo": {
"name": "Bluesky",
"slug": "bluesky",
"scheme": "bluesky",
"owner": "blueskysocial",
"version": "1.29.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/cloud-splash.png",
"resizeMode": "cover",
"backgroundColor": "#ffffff"
},
"ios": {
"buildNumber": "3",
"supportsTablet": false,
"bundleIdentifier": "xyz.blueskyweb.app",
"config": {
"usesNonExemptEncryption": false
},
"infoPlist": {
"UIBackgroundModes": [
"fetch",
"processing"
],
"BGTaskSchedulerPermittedIdentifiers": [
"com.transistorsoft.fetch"
],
"NSCameraUsageDescription": "Used for profile pictures, posts, and other kinds of content.",
"NSMicrophoneUsageDescription": "Used for posts and other kinds of content.",
"NSPhotoLibraryAddUsageDescription": "Used to save images to your library.",
"NSPhotoLibraryUsageDescription": "Used for profile pictures, posts, and other kinds of content"
},
"associatedDomains": ["applinks:bsky.app", "applinks:staging.bsky.app"]
},
"androidStatusBar": {
"barStyle": "dark-content",
"backgroundColor": "#ffffff"
},
"android": {
"versionCode": 15,
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "xyz.blueskyweb.app",
"intentFilters": [
{
"action": "VIEW",
"autoVerify": true,
"data": [
{
"scheme": "https",
"host": "bsky.app"
}
],
"category": ["BROWSABLE", "DEFAULT"]
}
]
},
"web": {
"favicon": "./assets/favicon.png"
},
"plugins": [
"expo-localization",
"react-native-background-fetch",
[
"expo-build-properties",
{
"android": {
"kotlinVersion": "1.8.0"
}
}
],
[
"expo-updates",
{
"username": "blueskysocial"
}
],
"sentry-expo"
],
"extra": {
"eas": {
"projectId": "55bd077a-d905-4184-9c7f-94789ba0f302"
}
},
"hooks": {
"postPublish": [
{
"file": "sentry-expo/upload-sourcemaps",
"config": {
"organization": "blueskyweb",
"project": "react-native"
}
}
]
}
}
}
+22 -1
View File
@@ -1,7 +1,23 @@
module.exports = function (api) {
api.cache(true)
const isTestEnv = process.env.NODE_ENV === 'test'
return {
presets: ['babel-preset-expo'],
presets: [
[
'babel-preset-expo',
{
lazyImports: true,
native: {
// We should be able to remove this after upgrading Expo
// to a version that includes https://github.com/expo/expo/pull/24672.
unstable_transformProfile: 'hermes-stable',
// Disable ESM -> CJS compilation because Metro takes care of it.
// However, we need it in Jest tests since those run without Metro.
disableImportExportTransform: !isTestEnv,
},
},
],
],
plugins: [
[
'module:react-native-dotenv',
@@ -30,5 +46,10 @@ module.exports = function (api) {
],
'react-native-reanimated/plugin', // NOTE: this plugin MUST be last
],
env: {
production: {
plugins: ['transform-remove-console'],
},
},
}
}
+4 -1
View File
@@ -2,6 +2,9 @@
SHELL = /bin/bash
.SHELLFLAGS = -o pipefail -c
# https://github.com/golang/go/wiki/LoopvarExperiment
export GOEXPERIMENT := loopvar
.PHONY: help
help: ## Print info about all commands
@echo "Commands:"
@@ -39,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
GOLOG_LOG_LEVEL=info go run ./cmd/bskyweb serve --debug
+2 -2
View File
@@ -2,7 +2,7 @@
### SPA Bundle (monolithic static javascript file)
To build the SPA bundle (`bundle.web.js`), first get a Javascript development
To build the SPA bundle (`bundle.web.js`), first get a JavaScript development
environment set up. Either follow the top-level README, or something quick
like:
@@ -24,7 +24,7 @@ Then build and copy over the big 'ol `bundle.web.js` file:
### Golang Daemon
Install golang. We are generally using v1.20+.
Install golang. We are generally using v1.21+.
In this directory (`bskyweb/`):
+1
View File
@@ -0,0 +1 @@
/bskyweb
+5 -3
View File
@@ -14,13 +14,15 @@ type Mailmodo struct {
httpClient *http.Client
APIKey string
BaseURL string
ListName string
}
func NewMailmodo(apiKey string) *Mailmodo {
func NewMailmodo(apiKey, listName string) *Mailmodo {
return &Mailmodo{
APIKey: apiKey,
BaseURL: "https://api.mailmodo.com/api/v1",
httpClient: &http.Client{},
ListName: listName,
}
}
@@ -56,9 +58,9 @@ func (m *Mailmodo) request(ctx context.Context, httpMethod string, apiMethod str
return nil
}
func (m *Mailmodo) AddToList(ctx context.Context, listName, email string) error {
func (m *Mailmodo) AddToList(ctx context.Context, email string) error {
return m.request(ctx, "POST", "addToList", map[string]any{
"listName": listName,
"listName": m.ListName,
"email": email,
"data": map[string]any{
"email_hashed": fmt.Sprintf("%x", sha256.Sum256([]byte(email))),
+5 -16
View File
@@ -34,22 +34,11 @@ func run(args []string) {
Action: serve,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "pds-host",
Usage: "method, hostname, and port of PDS instance",
Value: "http://localhost:4849",
EnvVars: []string{"ATP_PDS_HOST"},
},
&cli.StringFlag{
Name: "handle",
Usage: "for PDS login",
Required: true,
EnvVars: []string{"ATP_AUTH_HANDLE"},
},
&cli.StringFlag{
Name: "password",
Usage: "for PDS login",
Required: true,
EnvVars: []string{"ATP_AUTH_PASSWORD"},
Name: "appview-host",
Usage: "method, hostname, and port of PDS instance",
Value: "http://localhost:2584",
// retain old PDS env var for easy transition
EnvVars: []string{"ATP_APPVIEW_HOST", "ATP_PDS_HOST"},
},
&cli.StringFlag{
Name: "mailmodo-api-key",
+191 -47
View File
@@ -2,77 +2,100 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/fs"
"io/ioutil"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
comatproto "github.com/bluesky-social/indigo/api/atproto"
appbsky "github.com/bluesky-social/indigo/api/bsky"
cliutil "github.com/bluesky-social/indigo/cmd/gosky/util"
"github.com/bluesky-social/indigo/xrpc"
"github.com/bluesky-social/social-app/bskyweb"
"github.com/flosch/pongo2/v6"
"github.com/klauspost/compress/gzhttp"
"github.com/klauspost/compress/gzip"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/urfave/cli/v2"
)
type Server struct {
xrpcc *xrpc.Client
echo *echo.Echo
httpd *http.Server
mailmodo *Mailmodo
xrpcc *xrpc.Client
}
func serve(cctx *cli.Context) error {
debug := cctx.Bool("debug")
httpAddress := cctx.String("http-address")
pdsHost := cctx.String("pds-host")
atpHandle := cctx.String("handle")
atpPassword := cctx.String("password")
appviewHost := cctx.String("appview-host")
mailmodoAPIKey := cctx.String("mailmodo-api-key")
mailmodoListName := cctx.String("mailmodo-list-name")
// Mailmodo client.
mailmodo := NewMailmodo(mailmodoAPIKey)
// Echo
e := echo.New()
// create a new session
// TODO: does this work with no auth at all?
// Mailmodo client.
mailmodo := NewMailmodo(mailmodoAPIKey, mailmodoListName)
// create a new session (no auth)
xrpcc := &xrpc.Client{
Client: cliutil.NewHttpClient(),
Host: pdsHost,
Auth: &xrpc.AuthInfo{
Handle: atpHandle,
},
Host: appviewHost,
}
auth, err := comatproto.ServerCreateSession(context.TODO(), xrpcc, &comatproto.ServerCreateSession_Input{
Identifier: xrpcc.Auth.Handle,
Password: atpPassword,
})
// httpd
var (
httpTimeout = 2 * time.Minute
httpMaxHeaderBytes = 2 * (1024 * 1024)
gzipMinSizeBytes = 1024 * 2
gzipCompressionLevel = gzip.BestSpeed
gzipExceptMIMETypes = []string{"image/png"}
)
// Wrap the server handler in a gzip handler to compress larger responses.
gzipHandler, err := gzhttp.NewWrapper(
gzhttp.MinSize(gzipMinSizeBytes),
gzhttp.CompressionLevel(gzipCompressionLevel),
gzhttp.ExceptContentTypes(gzipExceptMIMETypes),
)
if err != nil {
return err
}
xrpcc.Auth.AccessJwt = auth.AccessJwt
xrpcc.Auth.RefreshJwt = auth.RefreshJwt
xrpcc.Auth.Did = auth.Did
xrpcc.Auth.Handle = auth.Handle
server := Server{xrpcc}
//
// server
//
server := &Server{
echo: e,
mailmodo: mailmodo,
xrpcc: xrpcc,
}
staticHandler := http.FileServer(func() http.FileSystem {
if debug {
return http.FS(os.DirFS("static"))
}
fsys, err := fs.Sub(bskyweb.StaticFS, "static")
if err != nil {
log.Fatal(err)
}
return http.FS(fsys)
}())
// Create the HTTP server.
server.httpd = &http.Server{
Handler: gzipHandler(server),
Addr: httpAddress,
WriteTimeout: httpTimeout,
ReadTimeout: httpTimeout,
MaxHeaderBytes: httpMaxHeaderBytes,
}
e := echo.New()
e.HideBanner = true
e.Renderer = NewRenderer("templates/", &bskyweb.TemplateFS, debug)
e.HTTPErrorHandler = server.errorHandler
e.IPExtractor = echo.ExtractIPFromXFFHeader()
// SECURITY: Do not modify without due consideration.
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
ContentTypeNosniff: "nosniff",
@@ -87,10 +110,24 @@ func serve(cctx *cli.Context) error {
Skipper: func(c echo.Context) bool {
return strings.HasPrefix(c.Request().URL.Path, "/static")
},
Format: "method=${method} path=${uri} status=${status} latency=${latency_human}\n",
}))
e.Renderer = NewRenderer("templates/", &bskyweb.TemplateFS, debug)
e.HTTPErrorHandler = customHTTPErrorHandler
e.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
Skipper: middleware.DefaultSkipper,
Store: middleware.NewRateLimiterMemoryStoreWithConfig(
middleware.RateLimiterMemoryStoreConfig{
Rate: 10, // requests per second
Burst: 30, // allow bursts
ExpiresIn: 3 * time.Minute, // garbage collect entries older than 3 minutes
},
),
IdentifierExtractor: func(ctx echo.Context) (string, error) {
id := ctx.RealIP()
return id, nil
},
DenyHandler: func(c echo.Context, identifier string, err error) error {
return c.String(http.StatusTooManyRequests, "Your request has been rate limited. Please try again later. Contact security@bsky.app if you believe this was a mistake.\n")
},
}))
// redirect trailing slash to non-trailing slash.
// all of our current endpoints have no trailing slash.
@@ -98,14 +135,50 @@ func serve(cctx *cli.Context) error {
RedirectCode: http.StatusFound,
}))
//
// configure routes
//
// static files
staticHandler := http.FileServer(func() http.FileSystem {
if debug {
log.Debugf("serving static file from the local file system")
return http.FS(os.DirFS("static"))
}
fsys, err := fs.Sub(bskyweb.StaticFS, "static")
if err != nil {
log.Fatal(err)
}
return http.FS(fsys)
}())
e.GET("/robots.txt", echo.WrapHandler(staticHandler))
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)))
e.GET("/ips-v4", echo.WrapHandler(staticHandler))
e.GET("/ips-v6", echo.WrapHandler(staticHandler))
e.GET("/.well-known/*", echo.WrapHandler(staticHandler))
e.GET("/security.txt", func(c echo.Context) error {
return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt")
})
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
path := c.Request().URL.Path
maxAge := 1 * (60 * 60) // default is 1 hour
// Cache javascript and images files for 1 week, which works because
// they're always versioned (e.g. /static/js/main.64c14927.js)
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/images/") {
maxAge = 7 * (60 * 60 * 24) // 1 week
}
c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", maxAge))
return next(c)
}
})
// home
e.GET("/", server.WebHome)
// generic routes
e.GET("/search", server.WebGeneric)
e.GET("/search/feeds", server.WebGeneric)
e.GET("/feeds", server.WebGeneric)
e.GET("/notifications", server.WebGeneric)
e.GET("/moderation", server.WebGeneric)
@@ -113,8 +186,11 @@ func serve(cctx *cli.Context) error {
e.GET("/moderation/muted-accounts", server.WebGeneric)
e.GET("/moderation/blocked-accounts", server.WebGeneric)
e.GET("/settings", server.WebGeneric)
e.GET("/settings/language", server.WebGeneric)
e.GET("/settings/app-passwords", server.WebGeneric)
e.GET("/settings/home-feed", server.WebGeneric)
e.GET("/settings/saved-feeds", server.WebGeneric)
e.GET("/settings/threads", server.WebGeneric)
e.GET("/sys/debug", server.WebGeneric)
e.GET("/sys/log", server.WebGeneric)
e.GET("/support", server.WebGeneric)
@@ -137,19 +213,54 @@ func serve(cctx *cli.Context) error {
e.GET("/profile/:handle/post/:rkey/reposted-by", server.WebGeneric)
// Mailmodo
e.POST("/waitlist", func(c echo.Context) error {
email := strings.TrimSpace(c.FormValue("email"))
if err := mailmodo.AddToList(c.Request().Context(), mailmodoListName, email); err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]bool{"success": true})
})
e.POST("/api/waitlist", server.apiWaitlist)
// Start the server.
log.Infof("starting server address=%s", httpAddress)
return e.Start(httpAddress)
go func() {
if err := server.httpd.ListenAndServe(); err != nil {
if !errors.Is(err, http.ErrServerClosed) {
log.Errorf("HTTP server shutting down unexpectedly: %s", err)
}
}
}()
// Wait for a signal to exit.
log.Info("registering OS exit signal handler")
quit := make(chan struct{})
exitSignals := make(chan os.Signal, 1)
signal.Notify(exitSignals, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-exitSignals
log.Infof("received OS exit signal: %s", sig)
// Shut down the HTTP server.
if err := server.Shutdown(); err != nil {
log.Errorf("HTTP server shutdown error: %s", err)
}
// Trigger the return that causes an exit.
close(quit)
}()
<-quit
log.Infof("graceful shutdown complete")
return nil
}
func customHTTPErrorHandler(err error, c echo.Context) {
func (srv *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
srv.echo.ServeHTTP(rw, req)
}
func (srv *Server) Shutdown() error {
log.Info("shutting down")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return srv.httpd.Shutdown(ctx)
}
func (srv *Server) errorHandler(err error, c echo.Context) {
code := http.StatusInternalServerError
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
@@ -225,3 +336,36 @@ func (srv *Server) WebProfile(c echo.Context) error {
return c.Render(http.StatusOK, "profile.html", data)
}
func (srv *Server) apiWaitlist(c echo.Context) error {
type jsonError struct {
Error string `json:"error"`
}
// Read the API request.
type apiRequest struct {
Email string `json:"email"`
}
bodyReader := http.MaxBytesReader(c.Response(), c.Request().Body, 16*1024)
payload, err := ioutil.ReadAll(bodyReader)
if err != nil {
return err
}
var req apiRequest
if err := json.Unmarshal(payload, &req); err != nil {
return c.JSON(http.StatusBadRequest, jsonError{Error: "Invalid API request"})
}
if req.Email == "" {
return c.JSON(http.StatusBadRequest, jsonError{Error: "Please enter a valid email address."})
}
if err := srv.mailmodo.AddToList(c.Request().Context(), req.Email); err != nil {
log.Errorf("adding email to waitlist failed: %s", err)
return c.JSON(http.StatusBadRequest, jsonError{
Error: "Storing email in waitlist failed. Please enter a valid email address.",
})
}
return c.JSON(http.StatusOK, map[string]bool{"success": true})
}
+1 -3
View File
@@ -1,4 +1,2 @@
GOLOG_LOG_LEVEL=info
ATP_PDS_HOST=http://localhost:2583
ATP_AUTH_HANDLE="admin.test"
ATP_AUTH_PASSWORD="admin"
ATP_APPVIEW_HOST=http://localhost:2584
+1 -3
View File
@@ -1,4 +1,2 @@
GOLOG_LOG_LEVEL=info
ATP_PDS_HOST=https://bsky.social
ATP_AUTH_HANDLE=TODO
ATP_AUTH_PASSWORD=TODO
ATP_APPVIEW_HOST=https://api.bsky.app
+2 -1
View File
@@ -1,12 +1,13 @@
module github.com/bluesky-social/social-app/bskyweb
go 1.20
go 1.21
require (
github.com/bluesky-social/indigo v0.0.0-20230504025040-8915cccc3319
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
)
+4
View File
@@ -31,6 +31,7 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
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/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=
@@ -105,6 +106,8 @@ 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=
@@ -238,6 +241,7 @@ 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/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=
@@ -0,0 +1,13 @@
{
"applinks": {
"apps": [],
"details": [
{
"appID": "B3LX46C5HS.xyz.blueskyweb.app",
"paths": [
"*"
]
}
]
}
}
@@ -0,0 +1,11 @@
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "xyz.blueskyweb.app",
"sha256_cert_fingerprints":
["C1:4D:3C:6B:B5:D6:D9:AE:CF:C5:0B:BC:C1:9B:29:6D:D4:E6:87:46:36:D5:4C:1A:64:1C:14:08:BF:7E:F9:62", "FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"]
}
}
]
+4
View File
@@ -0,0 +1,4 @@
Contact: mailto:security@bsky.app
Preferred-Languages: en
Canonical: https://bsky.app/.well-known/security.txt
Acknowledgements: https://github.com/bluesky-social/atproto/blob/main/CONTRIBUTORS.md
+30
View File
@@ -0,0 +1,30 @@
13.59.225.103/32
3.18.47.21/32
18.191.104.94/32
3.129.134.255/32
3.129.237.113/32
3.138.56.230/32
44.218.10.163/32
54.89.116.251/32
44.217.166.202/32
54.208.221.149/32
54.166.110.54/32
54.208.146.65/32
3.129.234.15/32
3.138.168.48/32
3.23.53.192/32
52.14.89.53/32
3.18.126.246/32
3.136.69.4/32
3.22.137.152/32
3.132.247.113/32
3.141.186.104/32
18.222.43.214/32
3.14.35.197/32
3.23.182.70/32
18.224.144.69/32
3.129.98.29/32
3.130.134.20/32
3.17.197.213/32
18.223.234.21/32
3.20.248.177/32
View File
+76 -15
View File
@@ -1,8 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, viewport-fit=cover">
<meta name="referrer" content="origin-when-cross-origin">
<title>{%- block head_title -%}Bluesky{%- endblock -%}</title>
@@ -40,6 +39,32 @@
height: calc(100% + env(safe-area-inset-top));
}
/* Color theming */
:root {
--text: black;
--background: white;
--backgroundLight: #F3F3F8;
}
html.colorMode--dark {
--text: white;
--background: black;
--backgroundLight: #26272D;
}
@media (prefers-color-scheme: light) {
html.colorMode--system {
--text: black;
--background: white;
--backgroundLight: #F3F3F8;
}
}
@media (prefers-color-scheme: dark) {
html.colorMode--system {
--text: white;
--background: black;
--backgroundLight: #26272D;
}
}
body {
display: flex;
/* Allows you to scroll below the viewport; default value is visible */
@@ -50,12 +75,6 @@
-moz-osx-font-smoothing: grayscale;
-ms-overflow-style: scrollbar;
}
/* Enable for apps that support dark-theme */
/*@media (prefers-color-scheme: dark) {
body {
background-color: black;
}
}*/
/* Remove default link styling */
a {
@@ -72,6 +91,9 @@
*[data-word-wrap] {
word-break: break-word;
}
*[data-stable-gutters] {
scrollbar-gutter: stable both-edges;
}
/* ProseMirror */
.ProseMirror {
@@ -94,26 +116,65 @@
.ProseMirror .mention {
color: #0085ff;
}
.ProseMirror a {
.ProseMirror a,
.ProseMirror .autolink {
color: #0085ff;
cursor: pointer;
}
/* OLLIE: TODO -- this is not accessible */
/* Remove focus state on inputs */
.ProseMirror-focused {
outline: 0;
}
textarea:focus,
input:focus {
outline: 0;
}
.tippy-content .items {
width: fit-content;
}
/* Tooltips */
[data-tooltip] {
position: relative;
z-index: 10;
}
[data-tooltip]::after {
content: attr(data-tooltip);
display: none;
position: absolute;
bottom: 0;
left: 50%;
transform: translateY(100%) translateY(8px) translateX(-50%);
padding: 4px 10px;
border-radius: 10px;
background: var(--backgroundLight);
color: var(--text);
text-align: center;
white-space: nowrap;
font-size: 12px;
z-index: 10;
}
[data-tooltip]::before {
content: '';
display: none;
position: absolute;
border-bottom: 6px solid var(--backgroundLight);
border-left: 6px solid transparent;
border-right: 6px solid transparent;
bottom: 0;
left: 50%;
transform: translateY(100%) translateY(2px) translateX(-50%);
z-index: 10;
}
[data-tooltip]:hover::after,
[data-tooltip]:hover::before {
display:block;
}
</style>
{% include "scripts.html" %}
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png"/>
<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"/>
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<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">
@@ -122,8 +183,8 @@
{%- block body_all %}
<div id="root"></div>
<noscript>
<h1>Javascript Required</h1>
<p>This is a heavily interactive web application, and Javascript is required. Simple HTML interfaces are possible, but that is not what this is.
<h1>JavaScript Required</h1>
<p>This is a heavily interactive web application, and JavaScript is required. Simple HTML interfaces are possible, but that is not what this is.
<p>Learn more about Bluesky at <a href="https://blueskyweb.xyz">blueskyweb.xyz</a> and <a href="https://atproto.com">atproto.com</a>.
{% block noscript_extra %}{% endblock %}
</noscript>
+7 -7
View File
@@ -3,13 +3,13 @@
{% block head_title %}Bluesky{% endblock %}
{% block html_head_extra -%}
<meta name="description" content="See what's next."/>
<meta property="og:type" content="website"/>
<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 name="twitter:card" content="summary"/>
<meta name="twitter:site" content="@bluesky"/>
<meta name="description" content="See what's next.">
<meta property="og:type" content="website">
<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 name="twitter:card" content="summary">
<meta name="twitter:site" content="@bluesky">
{%- endblock %}
{% block noscript_extra %}
+1 -1
View File
@@ -43,6 +43,6 @@
<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.Text }}</p>
<p id="bsky_post_text">{{ postView.Record.Val.Text }}</p>
</div>
{%- endblock %}
+23 -5
View File
@@ -8,12 +8,14 @@
- brew tap wix/brew
- brew install applesimutils
- After initial setup:
- `npx expo prebuild` -> you will also need to run this anytime `app.json` or `package.json` changes
- 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
- Start the dev servers
- `git clone git@github.com:bluesky-social/atproto.git`
- `cd atproto`
- `yarn`
- `cd packages/dev-env && yarn start`
- `pnpm i`
- `pnpm build`
- `cd packages/dev-env && pnpm start`
- Run the dev app
- iOS: `yarn ios`
- Android: `yarn android`
@@ -26,13 +28,23 @@
- Run once: `yarn e2e:build`
- Each test run: `yarn e2e:run`
- Tips
- Make sure you copy the `.env.example` to `.env` and add the appropiate tokens (e.g. `SENTRY_AUTH_TOKEN` can be created on the Sentry dashboard). If this is not required, you can remove it from `eas.json` and `package.json`, as well as any mentions in the code.
- 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.
- `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}`
- For instance, the localhosted dev-wallet will need `adb reverse tcp:3001 tcp:3001`
- 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).)
### Adding Sentry
Adding Sentry is NOT required. You can keep `SENTRY_AUTH_TOKEN=` in `.env` which will build the app without Sentry.
However, if you're a part of the Bluesky team and want to enable Sentry, fill in `SENTRY_AUTH_TOKEN` in your `.env`. It can be created on the Sentry dashboard using [these instructions](https://docs.expo.dev/guides/using-sentry/#sign-up-for-a-sentry-account-and-create-a-project).
If you change `SENTRY_AUTH_TOKEN`, you need to do `yarn prebuild` before running `yarn ios` or `yarn android` again.
## Go-Server Build
### Prerequisites
@@ -107,3 +119,9 @@ upload-sourcemaps \
--dist <iOS Update ID> \
--rewrite \
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/)
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/)
+14
View File
@@ -0,0 +1,14 @@
# Testing instructions
### 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.
3. You can also use [Maestro Studio](https://maestro.mobile.dev/getting-started/maestro-studio) which automatically generates commands by recording your actions on the app. Therefore, you can create realistic tests without having to manually write any code. Use the `maestro studio` command to start recording your actions.
### Using Flashlight for Performance Testing
1. Make sure Maestro is installed (optional: only for automated testing) by following the instructions above
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`.
+18 -3
View File
@@ -9,22 +9,37 @@
"distribution": "internal",
"ios": {
"simulator": true,
"resourceClass": "medium"
"resourceClass": "m-large"
},
"channel": "development"
},
"development-device": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"resourceClass": "m-large"
},
"channel": "development"
},
"preview": {
"distribution": "internal",
"ios": {
"resourceClass": "medium"
"resourceClass": "m-large"
},
"channel": "preview"
},
"production": {
"ios": {
"resourceClass": "medium"
"resourceClass": "m-large"
},
"channel": "production"
},
"dev-android-apk": {
"developmentClient": true,
"android": {
"buildType": "apk",
"gradleCommand": ":app:assembleRelease"
}
}
},
"submit": {
+41
View File
@@ -0,0 +1,41 @@
{
"project_info": {
"project_id": "blueskyweb-example",
"project_number": "100000000000",
"firebase_url": "https://blueskyweb-example.firebaseio.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:123456789000:android:f1bf012572b04063",
"android_client_info": {
"package_name": "xyz.blueskyweb.app"
}
},
"oauth_client": [
{
"client_id": "123456789000.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "123456789000"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
}
],
"configuration_version": "1"
}
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env sh
get_container_id() {
local compose_file=$1
local service=$2
if [ -z "${compose_file}" ] || [ -z "${service}" ]; then
echo "usage: get_container_id <compose_file> <service>"
exit 1
fi
docker compose -f $compose_file ps --format json --status running \
| jq -r '.[]? | select(.Service == "'${service}'") | .ID'
}
# Exports all environment variables
export_env() {
export_pg_env
export_redis_env
}
# Exports postgres environment variables
export_pg_env() {
# Based on creds in compose.yaml
export PGPORT=5433
export PGHOST=localhost
export PGUSER=pg
export PGPASSWORD=password
export PGDATABASE=postgres
export DB_POSTGRES_URL="postgresql://pg:password@127.0.0.1:5433/postgres"
}
# Exports redis environment variables
export_redis_env() {
export REDIS_HOST="127.0.0.1:6380"
}
# Main entry point
main() {
# Expect a SERVICES env var to be set with the docker service names
local services=${SERVICES}
dir=$(dirname $0)
compose_file="${dir}/docker-compose.yaml"
# whether this particular script started the container(s)
started_container=false
# trap SIGINT and performs cleanup as necessary, i.e.
# taking down containers if this script started them
trap "on_sigint ${services}" INT
on_sigint() {
local services=$@
echo # newline
if $started_container; then
docker compose -f $compose_file rm -f --stop --volumes ${services}
fi
exit $?
}
# check if all services are running already
not_running=false
for service in $services; do
container_id=$(get_container_id $compose_file $service)
if [ -z $container_id ]; then
not_running=true
break
fi
done
# if any are missing, recreate all services
if $not_running; then
docker compose -f $compose_file up --wait --force-recreate ${services}
started_container=true
else
echo "all services ${services} are already running"
fi
# setup environment variables and run args
export_env
"$@"
# save return code for later
code=$?
# performs cleanup as necessary, i.e. taking down containers
# if this script started them
echo # newline
if $started_container; then
docker compose -f $compose_file rm -f --stop --volumes ${services}
fi
exit ${code}
}
+49
View File
@@ -0,0 +1,49 @@
version: '3.8'
services:
# An ephermerally-stored postgres database for single-use test runs
db_test: &db_test
image: postgres:14.4-alpine
environment:
- POSTGRES_USER=pg
- POSTGRES_PASSWORD=password
ports:
- '5433:5432'
# Healthcheck ensures db is queryable when `docker-compose up --wait` completes
healthcheck:
test: 'pg_isready -U pg'
interval: 500ms
timeout: 10s
retries: 20
# A persistently-stored postgres database
db:
<<: *db_test
ports:
- '5432:5432'
healthcheck:
disable: true
volumes:
- atp_db:/var/lib/postgresql/data
# An ephermerally-stored redis cache for single-use test runs
redis_test: &redis_test
image: redis:7.0-alpine
ports:
- '6380:6379'
# Healthcheck ensures redis is queryable when `docker-compose up --wait` completes
healthcheck:
test: ['CMD-SHELL', '[ "$$(redis-cli ping)" = "PONG" ]']
interval: 500ms
timeout: 10s
retries: 20
# A persistently-stored redis cache
redis:
<<: *redis_test
command: redis-server --save 60 1 --loglevel warning
ports:
- '6379:6379'
healthcheck:
disable: true
volumes:
- atp_redis:/data
volumes:
atp_db:
atp_redis:
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env sh
# Example usage:
# ./with-test-db.sh psql postgresql://pg:password@localhost:5433/postgres -c 'select 1;'
dir=$(dirname $0)
. ${dir}/_common.sh
SERVICES="db_test" main "$@"
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env sh
# Example usage:
# ./with-test-redis-and-db.sh psql postgresql://pg:password@localhost:5433/postgres -c 'select 1;'
# ./with-test-redis-and-db.sh redis-cli -h localhost -p 6380 ping
dir=$(dirname $0)
. ${dir}/_common.sh
SERVICES="db_test redis_test" main "$@"
+11
View File
@@ -74,3 +74,14 @@ jest.mock('lande', () => ({
__esModule: true, // this property makes it work
default: jest.fn().mockReturnValue([['eng']]),
}))
jest.mock('sentry-expo', () => ({
init: () => jest.fn(),
Native: {
ReactNativeTracing: jest.fn().mockImplementation(() => ({
start: jest.fn(),
stop: jest.fn(),
})),
ReactNavigationInstrumentation: jest.fn(),
},
}))
+181 -142
View File
@@ -1,19 +1,8 @@
import {AddressInfo} from 'net'
import os from 'os'
import net from 'net'
import path from 'path'
import fs from 'fs'
import * as crypto from '@atproto/crypto'
import {PDS, ServerConfig, Database, MemoryBlobStore} from '@atproto/pds'
import * as plc from '@did-plc/lib'
import {PlcServer, Database as PlcDatabase} from '@did-plc/server'
import {BskyAgent} from '@atproto/api'
const ADMIN_PASSWORD = 'admin-pass'
const SECOND = 1000
const MINUTE = SECOND * 60
const HOUR = MINUTE * 60
const DAY = HOUR * 24
import {TestNetwork} from '@atproto/dev-env'
import {AtUri, BskyAgent} from '@atproto/api'
export interface TestUser {
email: string
@@ -29,95 +18,72 @@ export interface TestPDS {
close: () => Promise<void>
}
class StringIdGenerator {
_nextId = [0]
constructor(
public _chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
) {}
next() {
const r = []
for (const char of this._nextId) {
r.unshift(this._chars[char])
}
this._increment()
return r.join('')
}
_increment() {
for (let i = 0; i < this._nextId.length; i++) {
const val = ++this._nextId[i]
if (val >= this._chars.length) {
this._nextId[i] = 0
} else {
return
}
}
this._nextId.push(0)
}
*[Symbol.iterator]() {
while (true) {
yield this.next()
}
}
}
const ids = new StringIdGenerator()
export async function createServer(
{inviteRequired}: {inviteRequired: boolean} = {inviteRequired: false},
): Promise<TestPDS> {
const repoSigningKey = await crypto.Secp256k1Keypair.create()
const plcRotationKey = await crypto.Secp256k1Keypair.create()
const port = await getPort()
const plcDb = PlcDatabase.mock()
const plcServer = PlcServer.create({db: plcDb})
const plcListener = await plcServer.start()
const plcPort = (plcListener.address() as AddressInfo).port
const plcUrl = `http://localhost:${plcPort}`
const recoveryKey = (await crypto.Secp256k1Keypair.create()).did()
const plcClient = new plc.Client(plcUrl)
const serverDid = await plcClient.createDid({
signingKey: repoSigningKey.did(),
rotationKeys: [recoveryKey, plcRotationKey.did()],
handle: 'localhost',
pds: `http://localhost:${port}`,
signer: plcRotationKey,
})
const blobstoreLoc = path.join(os.tmpdir(), crypto.randomStr(5, 'base32'))
const cfg = new ServerConfig({
debugMode: true,
version: '0.0.0',
scheme: 'http',
hostname: 'localhost',
port,
serverDid,
recoveryKey,
adminPassword: ADMIN_PASSWORD,
inviteRequired,
didPlcUrl: plcUrl,
didCacheMaxTTL: DAY,
didCacheStaleTTL: HOUR,
jwtSecret: 'jwt-secret',
availableUserDomains: ['.test'],
appUrlPasswordReset: 'app://forgot-password',
emailNoReplyAddress: 'noreply@blueskyweb.xyz',
publicUrl: `http://localhost:${port}`,
imgUriSalt: '9dd04221f5755bce5f55f47464c27e1e',
imgUriKey:
'f23ecd142835025f42c3db2cf25dd813956c178392760256211f9d315f8ab4d8',
dbPostgresUrl: process.env.DB_POSTGRES_URL,
blobstoreLocation: `${blobstoreLoc}/blobs`,
blobstoreTmp: `${blobstoreLoc}/tmp`,
maxSubscriptionBuffer: 200,
repoBackfillLimitMs: HOUR,
userInviteInterval: 1,
labelerDid: 'did:example:labeler',
labelerKeywords: {},
})
const db =
cfg.dbPostgresUrl !== undefined
? Database.postgres({
url: cfg.dbPostgresUrl,
schema: cfg.dbPostgresSchema,
})
: Database.memory()
await db.migrateToLatestOrThrow()
const blobstore = new MemoryBlobStore()
const pds = PDS.create({
db,
blobstore,
repoSigningKey,
plcRotationKey,
config: cfg,
})
await pds.start()
const port2 = await getPort(port + 1)
const pdsUrl = `http://localhost:${port}`
const id = ids.next()
const testNet = await TestNetwork.create({
pds: {
port,
publicUrl: pdsUrl,
inviteRequired,
dbPostgresSchema: `pds_${id}`,
},
bsky: {
dbPostgresSchema: `bsky_${id}`,
},
plc: {port: port2},
})
const profilePic = fs.readFileSync(
const pic = fs.readFileSync(
path.join(__dirname, '..', 'assets', 'default-avatar.jpg'),
)
return {
pdsUrl,
mocker: new Mocker(pds, pdsUrl, profilePic),
mocker: new Mocker(testNet, pdsUrl, pic),
async close() {
await pds.destroy()
await plcServer.destroy()
await testNet.pds.server.destroy()
await testNet.plc.server.destroy()
},
}
}
@@ -127,13 +93,25 @@ class Mocker {
users: Record<string, TestUser> = {}
constructor(
public pds: PDS,
public testNet: TestNetwork,
public service: string,
public profilePic: Uint8Array,
public pic: Uint8Array,
) {
this.agent = new BskyAgent({service})
}
get pds() {
return this.testNet.pds
}
get bsky() {
return this.testNet.bsky
}
get plc() {
return this.testNet.plc
}
// NOTE
// deterministic date generator
// we use this to ensure the mock dataset is always the same
@@ -152,7 +130,7 @@ class Mocker {
const inviteRes = await agent.api.com.atproto.server.createInviteCode(
{useCount: 1},
{
headers: {authorization: `Basic ${btoa(`admin:${ADMIN_PASSWORD}`)}`},
headers: this.pds.adminAuthHeaders('admin'),
encoding: 'application/json',
},
)
@@ -165,7 +143,7 @@ class Mocker {
password: 'hunter2',
})
await agent.upsertProfile(async () => {
const blob = await agent.uploadBlob(this.profilePic, {
const blob = await agent.uploadBlob(this.pic, {
encoding: 'image/jpeg',
})
return {
@@ -221,6 +199,26 @@ class Mocker {
}
return await agent.post({
text,
langs: ['en'],
createdAt: new Date().toISOString(),
})
}
async createImagePost(user: string, text: string) {
const agent = this.users[user]?.agent
if (!agent) {
throw new Error(`Not a user: ${user}`)
}
const blob = await agent.uploadBlob(this.pic, {
encoding: 'image/jpeg',
})
return await agent.post({
text,
langs: ['en'],
embed: {
$type: 'app.bsky.embed.images',
images: [{image: blob.data.blob, alt: ''}],
},
createdAt: new Date().toISOString(),
})
}
@@ -237,6 +235,7 @@ class Mocker {
return await agent.post({
text,
embed: {$type: 'app.bsky.embed.record', record: {uri, cid}},
langs: ['en'],
createdAt: new Date().toISOString(),
})
}
@@ -253,6 +252,7 @@ class Mocker {
return await agent.post({
text,
reply: {root: {uri, cid}, parent: {uri, cid}},
langs: ['en'],
createdAt: new Date().toISOString(),
})
}
@@ -265,29 +265,72 @@ class Mocker {
return await agent.like(uri, cid)
}
async createFeed(user: string, rkey: string, posts: string[]) {
const agent = this.users[user]?.agent
if (!agent) {
throw new Error(`Not a user: ${user}`)
}
const fgUri = AtUri.make(
this.users[user].did,
'app.bsky.feed.generator',
rkey,
)
const fg1 = await this.testNet.createFeedGen({
[fgUri.toString()]: async () => {
return {
encoding: 'application/json',
body: {
feed: posts.slice(0, 30).map(uri => ({post: uri})),
},
}
},
})
const avatarRes = await agent.api.com.atproto.repo.uploadBlob(this.pic, {
encoding: 'image/png',
})
return await agent.api.app.bsky.feed.generator.create(
{repo: this.users[user].did, rkey},
{
did: fg1.did,
displayName: rkey,
description: 'all my fav stuff',
avatar: avatarRes.data.blob,
createdAt: new Date().toISOString(),
},
)
}
async createInvite(forAccount: string) {
const agent = new BskyAgent({service: this.agent.service})
await agent.api.com.atproto.server.createInviteCode(
{useCount: 1, forAccount},
{
headers: this.pds.adminAuthHeaders('admin'),
encoding: 'application/json',
},
)
}
async labelAccount(label: string, user: string) {
const did = this.users[user]?.did
if (!did) {
throw new Error(`Invalid user: ${user}`)
}
const ctx = this.pds.ctx
const ctx = this.bsky.ctx
if (!ctx) {
throw new Error('Invalid PDS')
throw new Error('Invalid appview')
}
await ctx.db.db
.insertInto('label')
.values([
{
src: ctx.cfg.labelerDid,
uri: did,
cid: '',
val: label,
neg: 0,
cts: new Date().toISOString(),
},
])
.execute()
const labelSrvc = ctx.services.label(ctx.db.getPrimary())
await labelSrvc.createLabels([
{
src: ctx.cfg.labelerDid,
uri: did,
cid: '',
val: label,
neg: false,
cts: new Date().toISOString(),
},
])
}
async labelProfile(label: string, user: string) {
@@ -302,43 +345,39 @@ class Mocker {
rkey: 'self',
})
const ctx = this.pds.ctx
const ctx = this.bsky.ctx
if (!ctx) {
throw new Error('Invalid PDS')
throw new Error('Invalid appview')
}
await ctx.db.db
.insertInto('label')
.values([
{
src: ctx.cfg.labelerDid,
uri: profile.uri,
cid: profile.cid,
val: label,
neg: 0,
cts: new Date().toISOString(),
},
])
.execute()
const labelSrvc = ctx.services.label(ctx.db.getPrimary())
await labelSrvc.createLabels([
{
src: ctx.cfg.labelerDid,
uri: profile.uri,
cid: profile.cid,
val: label,
neg: false,
cts: new Date().toISOString(),
},
])
}
async labelPost(label: string, {uri, cid}: {uri: string; cid: string}) {
const ctx = this.pds.ctx
const ctx = this.bsky.ctx
if (!ctx) {
throw new Error('Invalid PDS')
throw new Error('Invalid appview')
}
await ctx.db.db
.insertInto('label')
.values([
{
src: ctx.cfg.labelerDid,
uri,
cid,
val: label,
neg: 0,
cts: new Date().toISOString(),
},
])
.execute()
const labelSrvc = ctx.services.label(ctx.db.getPrimary())
await labelSrvc.createLabels([
{
src: ctx.cfg.labelerDid,
uri,
cid,
val: label,
neg: false,
cts: new Date().toISOString(),
},
])
}
async createMuteList(user: string, name: string): Promise<string> {
@@ -380,8 +419,8 @@ const checkAvailablePort = (port: number) =>
})
})
async function getPort() {
for (let i = 3000; i < 65000; i++) {
async function getPort(start = 3000) {
for (let i = start; i < 65000; i++) {
if (await checkAvailablePort(i)) {
return i
}
+18
View File
@@ -1,7 +1,25 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const {getDefaultConfig} = require('expo/metro-config')
const cfg = getDefaultConfig(__dirname)
cfg.resolver.sourceExts = process.env.RN_SRC_EXT
? process.env.RN_SRC_EXT.split(',').concat(cfg.resolver.sourceExts)
: cfg.resolver.sourceExts
cfg.transformer.getTransformOptions = async () => ({
transform: {
experimentalImportSupport: true,
inlineRequires: true,
nonInlinedRequires: [
// We can remove this option and rely on the default after
// https://github.com/facebook/metro/pull/1126 is released.
'React',
'react',
'react/jsx-dev-runtime',
'react/jsx-runtime',
'react-native',
],
},
})
module.exports = cfg
+84 -52
View File
@@ -1,44 +1,56 @@
{
"name": "bsky.app",
"version": "1.29.0",
"version": "1.55.0",
"private": true,
"scripts": {
"prepare": "is-ci || husky install",
"postinstall": "patch-package",
"prebuild": "expo prebuild",
"prebuild": "expo prebuild --clean",
"android": "expo run:android",
"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/",
"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",
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx",
"e2e:mock-server": "ts-node __e2e__/mock-server.ts",
"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"
"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"
},
"dependencies": {
"@atproto/api": "0.3.8",
"@atproto/api": "^0.6.21",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@expo/webpack-config": "^18.0.1",
"@emoji-mart/react": "^1.1.1",
"@expo/html-elements": "^0.4.2",
"@expo/webpack-config": "^19.0.0",
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-native-fontawesome": "^0.3.0",
"@gorhom/bottom-sheet": "^4",
"@mattermost/react-native-paste-input": "^0.6.0",
"@miblanchard/react-native-slider": "^2.2.0",
"@notifee/react-native": "^7.4.0",
"@react-native-async-storage/async-storage": "^1.17.6",
"@gorhom/bottom-sheet": "^4.5.1",
"@mattermost/react-native-paste-input": "^0.6.4",
"@miblanchard/react-native-slider": "^2.3.1",
"@react-native-async-storage/async-storage": "1.18.2",
"@react-native-camera-roll/camera-roll": "^5.2.2",
"@react-native-clipboard/clipboard": "^1.10.0",
"@react-native-community/blur": "^4.3.0",
"@react-native-community/datetimepicker": "6.7.3",
"@react-native-community/datetimepicker": "7.2.0",
"@react-native-menu/menu": "^0.8.0",
"@react-native-picker/picker": "2.4.10",
"@react-navigation/bottom-tabs": "^6.5.7",
"@react-navigation/drawer": "^6.6.2",
"@react-navigation/native": "^6.1.6",
@@ -47,53 +59,58 @@
"@segment/analytics-react": "^1.0.0-rc1",
"@segment/analytics-react-native": "^2.10.1",
"@segment/sovran-react-native": "^0.4.5",
"@sentry/react-native": "4.13.0",
"@sentry/react-native": "5.10.0",
"@tanstack/react-query": "^4.33.0",
"@tiptap/core": "^2.0.0-beta.220",
"@tiptap/extension-document": "^2.0.0-beta.220",
"@tiptap/extension-hard-break": "^2.0.3",
"@tiptap/extension-history": "^2.0.3",
"@tiptap/extension-link": "^2.0.0-beta.220",
"@tiptap/extension-mention": "^2.0.0-beta.220",
"@tiptap/extension-paragraph": "^2.0.0-beta.220",
"@tiptap/extension-placeholder": "^2.0.0-beta.220",
"@tiptap/extension-text": "^2.0.0-beta.220",
"@tiptap/html": "^2.1.11",
"@tiptap/pm": "^2.0.0-beta.220",
"@tiptap/react": "^2.0.0-beta.220",
"@tiptap/suggestion": "^2.0.0-beta.220",
"@types/node": "^18.16.2",
"@zxing/text-encoding": "^0.9.0",
"array.prototype.findlast": "^1.2.3",
"await-lock": "^2.2.2",
"babel-plugin-transform-remove-console": "^6.9.4",
"base64-js": "^1.5.1",
"bcp-47-match": "^2.0.3",
"email-validator": "^2.0.4",
"eslint-plugin-react-native-a11y": "^3.3.0",
"expo": "~48.0.15",
"expo-application": "~5.1.1",
"expo-build-properties": "~0.5.1",
"expo-camera": "~13.2.1",
"expo-constants": "~14.2.1",
"expo-dev-client": "~2.1.1",
"expo-device": "~5.2.1",
"expo-image": "^1.2.3",
"expo-image-manipulator": "^11.1.1",
"expo-image-picker": "^14.1.1",
"expo-localization": "~14.1.1",
"expo-media-library": "~15.2.3",
"expo-sharing": "~11.2.2",
"expo-splash-screen": "~0.18.2",
"expo-status-bar": "~1.4.4",
"expo-system-ui": "~2.2.1",
"expo-updates": "~0.16.4",
"emoji-mart": "^5.5.2",
"eventemitter3": "^5.0.1",
"expo": "^49.0.8",
"expo-application": "~5.3.0",
"expo-build-properties": "~0.8.3",
"expo-camera": "13.5.1",
"expo-constants": "~14.4.2",
"expo-dev-client": "2.4.7",
"expo-device": "~5.4.0",
"expo-image": "~1.3.2",
"expo-image-manipulator": "~11.5.0",
"expo-image-picker": "~14.5.0",
"expo-localization": "~14.3.0",
"expo-media-library": "~15.4.1",
"expo-notifications": "~0.20.1",
"expo-sharing": "~11.5.0",
"expo-splash-screen": "~0.20.5",
"expo-status-bar": "~1.6.0",
"expo-system-ui": "~2.4.0",
"expo-updates": "~0.18.12",
"fast-text-encoding": "^1.0.6",
"graphemer": "^1.4.0",
"he": "^1.2.0",
"history": "^5.3.0",
"js-sha256": "^0.9.0",
"lande": "^1.0.10",
"lodash.chunk": "^4.2.0",
"lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"lodash.omit": "^4.5.0",
"lodash.once": "^4.1.1",
"lodash.random": "^3.2.0",
"lodash.samplesize": "^4.2.0",
"lodash.set": "^4.3.2",
"lodash.shuffle": "^4.2.0",
@@ -104,48 +121,52 @@
"normalize-url": "^8.0.0",
"patch-package": "^6.5.1",
"postinstall-postinstall": "^2.1.0",
"psl": "^1.9.0",
"react": "18.2.0",
"react-avatar-editor": "^13.0.0",
"react-circular-progressbar": "^2.1.0",
"react-dom": "^18.2.0",
"react-native": "0.71.7",
"react-native": "0.72.5",
"react-native-appstate-hook": "^1.0.6",
"react-native-background-fetch": "^4.1.8",
"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.9.0",
"react-native-gesture-handler": "^2.12.1",
"react-native-get-random-values": "^1.8.0",
"react-native-haptic-feedback": "^1.14.0",
"react-native-image-crop-picker": "^0.38.1",
"react-native-inappbrowser-reborn": "^3.6.3",
"react-native-ios-context-menu": "^1.15.3",
"react-native-linear-gradient": "^2.6.2",
"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": "~2.14.4",
"react-native-reanimated": "^3.4.2",
"react-native-root-siblings": "^4.1.1",
"react-native-safe-area-context": "^4.4.1",
"react-native-screens": "^3.13.1",
"react-native-safe-area-context": "4.6.3",
"react-native-screens": "~3.22.0",
"react-native-splash-screen": "^3.3.0",
"react-native-svg": "13.4.0",
"react-native-svg": "13.9.0",
"react-native-url-polyfill": "^1.3.0",
"react-native-uuid": "^2.0.1",
"react-native-version-number": "^0.3.6",
"react-native-web": "^0.18.11",
"react-native-web": "~0.19.6",
"react-native-web-linear-gradient": "^1.1.2",
"react-responsive": "^9.0.2",
"rn-fetch-blob": "^0.12.0",
"sentry-expo": "~6.1.0",
"sentry-expo": "~7.0.1",
"tippy.js": "^6.3.7",
"tlds": "^1.234.0",
"zeego": "^1.6.2",
"zod": "^3.20.2"
},
"devDependencies": {
"@atproto/pds": "^0.1.10",
"@atproto/dev-env": "^0.2.5",
"@babel/core": "^7.20.0",
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@did-plc/server": "^0.0.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@react-native-community/eslint-config": "^3.0.0",
"@testing-library/jest-native": "^5.4.1",
"@testing-library/react-native": "^11.5.2",
@@ -153,15 +174,16 @@
"@types/he": "^1.1.2",
"@types/jest": "^29.4.0",
"@types/lodash.chunk": "^4.2.7",
"@types/lodash.clonedeep": "^4.5.7",
"@types/lodash.debounce": "^4.0.7",
"@types/lodash.isequal": "^4.5.6",
"@types/lodash.omit": "^4.5.7",
"@types/lodash.once": "^4.1.7",
"@types/lodash.random": "^3.2.7",
"@types/lodash.samplesize": "^4.2.7",
"@types/lodash.set": "^4.3.7",
"@types/lodash.shuffle": "^4.2.7",
"@types/psl": "^1.1.1",
"@types/react-avatar-editor": "^13.0.0",
"@types/react-native": "^0.67.3",
"@types/react-responsive": "^8.0.5",
"@types/react-test-renderer": "^17.0.1",
"@typescript-eslint/eslint-plugin": "^5.48.2",
@@ -170,28 +192,35 @@
"babel-loader": "^9.1.2",
"babel-plugin-module-resolver": "^5.0.0",
"babel-plugin-react-native-web": "^0.18.12",
"detox": "^20.1.2",
"detox": "^20.13.0",
"eslint": "^8.19.0",
"eslint-plugin-detox": "^1.0.0",
"eslint-plugin-ft-flow": "^2.0.3",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-native-a11y": "^3.3.0",
"html-webpack-plugin": "^5.5.0",
"husky": "^8.0.3",
"is-ci": "^3.0.1",
"jest": "^29.4.3",
"jest-expo": "^48.0.2",
"jest-expo": "^49.0.0",
"jest-junit": "^15.0.0",
"lint-staged": "^13.2.3",
"metro-react-native-babel-preset": "^0.73.7",
"prettier": "^2.8.3",
"react-native-dotenv": "^3.3.1",
"react-refresh": "^0.14.0",
"react-scripts": "^5.0.1",
"react-test-renderer": "18.2.0",
"ts-node": "^10.9.1",
"typescript": "^4.4.4",
"typescript": "^5.1.3",
"url-loader": "^4.1.1",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
},
"resolutions": {
"@types/react": "^17"
"@types/react": "^18",
"**/zeed-dom": "0.10.9"
},
"jest": {
"preset": "jest-expo/ios",
@@ -208,7 +237,7 @@
"node"
],
"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)"
"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)"
],
"modulePathIgnorePatterns": [
"__tests__/.*/__mocks__",
@@ -238,5 +267,8 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"lint-staged": {
"*{.js,.jsx,.ts,.tsx}": "yarn eslint --fix"
}
}
@@ -51,8 +51,7 @@ index 0000000..8c9c699
+minAndroidGradlePluginVersion=1.0.0
diff --git a/node_modules/@mattermost/react-native-paste-input/android/build/intermediates/compile_r_class_jar/debug/R.jar b/node_modules/@mattermost/react-native-paste-input/android/build/intermediates/compile_r_class_jar/debug/R.jar
new file mode 100644
index 0000000..c0106e0
Binary files /dev/null and b/node_modules/@mattermost/react-native-paste-input/android/build/intermediates/compile_r_class_jar/debug/R.jar differ
index 0000000..e69de29
diff --git a/node_modules/@mattermost/react-native-paste-input/android/build/intermediates/compile_symbol_list/debug/R.txt b/node_modules/@mattermost/react-native-paste-input/android/build/intermediates/compile_symbol_list/debug/R.txt
new file mode 100644
index 0000000..7c9d30e
+15
View File
@@ -0,0 +1,15 @@
diff --git a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
index 7e0b4cd..177454c 100644
--- a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
+++ b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
@@ -3,6 +3,8 @@ import { LogBox } from 'react-native';
* This is a workaround for using fetch on RN, this is a known issue in react-native and only generates a warning.
*/
export function ignoreRequireCycleLogs() {
- LogBox.ignoreLogs(['Require cycle:']);
+ try {
+ LogBox.ignoreLogs(['Require cycle:']);
+ } catch (e) {}
}
//# sourceMappingURL=ignorerequirecyclelogs.js.map
\ No newline at end of file
+14
View File
@@ -0,0 +1,14 @@
diff --git a/node_modules/babel-preset-expo/index.js b/node_modules/babel-preset-expo/index.js
index 2099ee3..2b9e092 100644
--- a/node_modules/babel-preset-expo/index.js
+++ b/node_modules/babel-preset-expo/index.js
@@ -105,7 +105,8 @@ module.exports = function (api, options = {}) {
],
],
plugins: [
- getObjectRestSpreadPlugin(),
+ // - dan: This will be disabled anyway when we upgrade Expo, but let's do it now.
+ // getObjectRestSpreadPlugin(),
...extraPlugins,
getAliasPlugin(),
[require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }],
+12
View File
@@ -0,0 +1,12 @@
diff --git a/node_modules/babel-preset-fbjs/plugins/inline-requires.js b/node_modules/babel-preset-fbjs/plugins/inline-requires.js
index b11fc83..e18661a 100644
--- a/node_modules/babel-preset-fbjs/plugins/inline-requires.js
+++ b/node_modules/babel-preset-fbjs/plugins/inline-requires.js
@@ -256,6 +256,7 @@ function getInlineableModule(path, state) {
return moduleName == null ||
state.ignoredRequires.has(moduleName) ||
+ moduleName.startsWith('@babel/runtime/') ||
isRequireInScope
? null
: { moduleName, requireFnName: fnName };
+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),
+50
View File
@@ -0,0 +1,50 @@
diff --git a/node_modules/metro-runtime/src/polyfills/require.js b/node_modules/metro-runtime/src/polyfills/require.js
index ce67cb4..eeeae84 100644
--- a/node_modules/metro-runtime/src/polyfills/require.js
+++ b/node_modules/metro-runtime/src/polyfills/require.js
@@ -22,6 +22,13 @@ global.__c = clear;
global.__registerSegment = registerSegment;
var modules = clear();
+if (__DEV__) {
+ // Added by Dan for module init logging.
+ global.__INIT_LOGS__ = []
+ var initModuleCounter = 0
+ var initModuleStack = []
+}
+
// Don't use a Symbol here, it would pull in an extra polyfill with all sorts of
// additional stuff (e.g. Array.from).
const EMPTY = {};
@@ -303,7 +310,30 @@ function loadModuleImplementation(moduleId, module) {
throw module.error;
}
if (__DEV__) {
- var Systrace = requireSystrace();
+ // Added by Dan for module init logging.
+ var Systrace = {
+ beginEvent(label) {
+ let fullLabel = initModuleCounter++ + ' ' + label
+ global.__INIT_LOGS__.push(
+ ' '.repeat(initModuleStack.length) +
+ ' ENTER ' + fullLabel
+ )
+ initModuleStack.push({
+ fullLabel,
+ startTime: nativePerformanceNow(),
+ })
+ },
+ endEvent() {
+ const res = initModuleStack.pop()
+ const fullLabel = res.fullLabel
+ const startTime = res.startTime
+ const timeElapsed = Math.round(nativePerformanceNow() - startTime)
+ global.__INIT_LOGS__.push(
+ ' '.repeat(initModuleStack.length) +
+ ' LEAVE ' + fullLabel + ' [' + timeElapsed + 'ms]',
+ )
+ }
+ };
var Refresh = requireRefresh();
}
@@ -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);
+17
View File
@@ -0,0 +1,17 @@
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m
index 9dca6a5..090bda5 100644
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m
+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m
@@ -266,11 +266,10 @@ static void *TextFieldSelectionObservingContext = &TextFieldSelectionObservingCo
- (void)textViewDidChangeSelection:(__unused UITextView *)textView
{
- if (_lastStringStateWasUpdatedWith && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
+ if (![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
[self textViewDidChange:_backedTextInputView];
_ignoreNextTextInputCall = YES;
}
- _lastStringStateWasUpdatedWith = _backedTextInputView.attributedText;
[self textViewProbablyDidChangeSelection];
}
+4 -4
View File
@@ -1,5 +1,5 @@
diff --git a/node_modules/react-native-pager-view/ios/ReactNativePageView.m b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
index ab0fc7f..fbbf19f 100644
index ab0fc7f..1ace752 100644
--- a/node_modules/react-native-pager-view/ios/ReactNativePageView.m
+++ b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
@@ -1,6 +1,6 @@
@@ -19,7 +19,7 @@ index ab0fc7f..fbbf19f 100644
@property(nonatomic, strong) UIPageViewController *reactPageViewController;
@property(nonatomic, strong) RCTEventDispatcher *eventDispatcher;
@@ -80,6 +80,10 @@
@@ -80,6 +80,10 @@ - (void)didMoveToWindow {
[self setupInitialController];
}
@@ -30,13 +30,13 @@ index ab0fc7f..fbbf19f 100644
if (self.reactViewController.navigationController != nil && self.reactViewController.navigationController.interactivePopGestureRecognizer != nil) {
[self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:self.reactViewController.navigationController.interactivePopGestureRecognizer];
}
@@ -463,4 +467,21 @@
@@ -463,4 +467,21 @@ - (NSString *)determineScrollDirection:(UIScrollView *)scrollView {
- (BOOL)isLtrLayout {
return [_layoutDirection isEqualToString:@"ltr"];
}
+
+- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
+ if (otherGestureRecognizer == self.scrollView.panGestureRecognizer) {
+ if (!_overdrag && otherGestureRecognizer == self.scrollView.panGestureRecognizer) {
+ UIPanGestureRecognizer* p = (UIPanGestureRecognizer*) gestureRecognizer;
+ CGPoint velocity = [p velocityInView:self];
+ if (self.currentIndex == 0 && velocity.x > 0) {
+29 -32
View File
@@ -1,45 +1,39 @@
import 'react-native-url-polyfill/auto'
import 'lib/sentry' // must be near top
import React, {useState, useEffect} from 'react'
import 'lib/sentry' // must be relatively on top
import {withSentry} from 'lib/sentry'
import {Linking} from 'react-native'
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 'view/icons'
import {withSentry} from 'lib/sentry'
import {ThemeProvider} from 'lib/ThemeContext'
import {s} from 'lib/styles'
import * as view from './view/index'
import {RootStoreModel, setupState, RootStoreProvider} from './state'
import {Shell} from './view/shell'
import * as notifee from 'lib/notifee'
import * as analytics from 'lib/analytics'
import * as Toast from './view/com/util/Toast'
import {handleLink} from './Navigation'
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'
SplashScreen.preventAutoHideAsync()
const App = observer(() => {
const App = observer(function AppImpl() {
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
undefined,
)
// init
useEffect(() => {
view.setup()
setupState().then(store => {
setRootStore(store)
analytics.init(store)
notifee.init(store)
SplashScreen.hideAsync()
Linking.getInitialURL().then((url: string | null) => {
if (url) {
handleLink(url)
}
})
Linking.addEventListener('url', ({url}) => {
handleLink(url)
})
notifications.init(store)
store.onSessionDropped(() => {
Toast.show('Sorry! Your session expired. Please log in again.')
})
@@ -51,17 +45,20 @@ const App = observer(() => {
return null
}
return (
<ThemeProvider theme={rootStore.shell.darkMode ? 'dark' : 'light'}>
<RootSiblingParent>
<analytics.Provider>
<RootStoreProvider value={rootStore}>
<GestureHandlerRootView style={s.h100pct}>
<Shell />
</GestureHandlerRootView>
</RootStoreProvider>
</analytics.Provider>
</RootSiblingParent>
</ThemeProvider>
<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>
</QueryClientProvider>
)
})
+28 -22
View File
@@ -1,23 +1,27 @@
import 'lib/sentry' // must be near top
import React, {useState, useEffect} from 'react'
import 'lib/sentry' // must be relatively on top
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 * as view from './view/index'
import * as analytics from 'lib/analytics'
import {RootStoreModel, setupState, RootStoreProvider} from './state'
import {Shell} from './view/shell/index'
import {ToastContainer} from './view/com/util/Toast.web'
import {ThemeProvider} from 'lib/ThemeContext'
import {observer} from 'mobx-react-lite'
const App = observer(() => {
import 'view/icons'
import * as analytics from 'lib/analytics/analytics'
import {RootStoreModel, setupState, RootStoreProvider} from './state'
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'
const App = observer(function AppImpl() {
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
undefined,
)
// init
useEffect(() => {
view.setup()
setupState().then(store => {
setRootStore(store)
analytics.init(store)
@@ -30,18 +34,20 @@ const App = observer(() => {
}
return (
<ThemeProvider theme={rootStore.shell.darkMode ? 'dark' : 'light'}>
<RootSiblingParent>
<analytics.Provider>
<RootStoreProvider value={rootStore}>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
<ToastContainer />
</RootStoreProvider>
</analytics.Provider>
</RootSiblingParent>
</ThemeProvider>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={rootStore.shell.colorMode}>
<RootSiblingParent>
<analytics.Provider>
<RootStoreProvider value={rootStore}>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
<ToastContainer />
</RootStoreProvider>
</analytics.Provider>
</RootSiblingParent>
</ThemeProvider>
</QueryClientProvider>
)
})
+144 -58
View File
@@ -1,5 +1,6 @@
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,
@@ -10,7 +11,10 @@ import {
DarkTheme,
} from '@react-navigation/native'
import {createNativeStackNavigator} from '@react-navigation/native-stack'
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'
import {
BottomTabBarProps,
createBottomTabNavigator,
} from '@react-navigation/bottom-tabs'
import {
HomeTabNavigatorParams,
SearchTabNavigatorParams,
@@ -30,6 +34,10 @@ 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 {HomeScreen} from './view/screens/Home'
import {SearchScreen} from './view/screens/Search'
@@ -37,9 +45,9 @@ import {FeedsScreen} from './view/screens/Feeds'
import {NotificationsScreen} from './view/screens/Notifications'
import {ModerationScreen} from './view/screens/Moderation'
import {ModerationMuteListsScreen} from './view/screens/ModerationMuteLists'
import {DiscoverFeedsScreen} from 'view/screens/DiscoverFeeds'
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'
@@ -60,8 +68,8 @@ import {AppPasswords} from 'view/screens/AppPasswords'
import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts'
import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
import {SavedFeeds} from 'view/screens/SavedFeeds'
import {getRoutingInstrumentation} from 'lib/sentry'
import {bskyTitle} from 'lib/strings/headings'
import {PreferencesHomeFeed} from 'view/screens/PreferencesHomeFeed'
import {PreferencesThreads} from 'view/screens/PreferencesThreads'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
@@ -84,133 +92,146 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
<>
<Stack.Screen
name="NotFound"
component={NotFoundScreen}
getComponent={() => NotFoundScreen}
options={{title: title('Not Found')}}
/>
<Stack.Screen
name="Moderation"
component={ModerationScreen}
getComponent={() => ModerationScreen}
options={{title: title('Moderation')}}
/>
<Stack.Screen
name="ModerationMuteLists"
component={ModerationMuteListsScreen}
getComponent={() => ModerationMuteListsScreen}
options={{title: title('Mute Lists')}}
/>
<Stack.Screen
name="ModerationMutedAccounts"
component={ModerationMutedAccounts}
getComponent={() => ModerationMutedAccounts}
options={{title: title('Muted Accounts')}}
/>
<Stack.Screen
name="ModerationBlockedAccounts"
component={ModerationBlockedAccounts}
getComponent={() => ModerationBlockedAccounts}
options={{title: title('Blocked Accounts')}}
/>
<Stack.Screen
name="DiscoverFeeds"
component={DiscoverFeedsScreen}
options={{title: title('Discover Feeds')}}
/>
<Stack.Screen
name="Settings"
component={SettingsScreen}
getComponent={() => SettingsScreen}
options={{title: title('Settings')}}
/>
<Stack.Screen
name="LanguageSettings"
getComponent={() => LanguageSettingsScreen}
options={{title: title('Language Settings')}}
/>
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={({route}) => ({title: title(`@${route.params.name}`)})}
getComponent={() => ProfileScreen}
options={({route}) => ({
title: title(`@${route.params.name}`),
animation: 'none',
})}
/>
<Stack.Screen
name="ProfileFollowers"
component={ProfileFollowersScreen}
getComponent={() => ProfileFollowersScreen}
options={({route}) => ({
title: title(`People following @${route.params.name}`),
})}
/>
<Stack.Screen
name="ProfileFollows"
component={ProfileFollowsScreen}
getComponent={() => ProfileFollowsScreen}
options={({route}) => ({
title: title(`People followed by @${route.params.name}`),
})}
/>
<Stack.Screen
name="ProfileList"
component={ProfileListScreen}
getComponent={() => ProfileListScreen}
options={{title: title('Mute List')}}
/>
<Stack.Screen
name="PostThread"
component={PostThreadScreen}
getComponent={() => PostThreadScreen}
options={({route}) => ({title: title(`Post by @${route.params.name}`)})}
/>
<Stack.Screen
name="PostLikedBy"
component={PostLikedByScreen}
getComponent={() => PostLikedByScreen}
options={({route}) => ({title: title(`Post by @${route.params.name}`)})}
/>
<Stack.Screen
name="PostRepostedBy"
component={PostRepostedByScreen}
getComponent={() => PostRepostedByScreen}
options={({route}) => ({title: title(`Post by @${route.params.name}`)})}
/>
<Stack.Screen
name="CustomFeed"
component={CustomFeedScreen}
getComponent={() => CustomFeedScreen}
options={{title: title('Feed')}}
/>
<Stack.Screen
name="CustomFeedLikedBy"
component={CustomFeedLikedByScreen}
getComponent={() => CustomFeedLikedByScreen}
options={{title: title('Liked by')}}
/>
<Stack.Screen
name="Debug"
component={DebugScreen}
getComponent={() => DebugScreen}
options={{title: title('Debug')}}
/>
<Stack.Screen
name="Log"
component={LogScreen}
getComponent={() => LogScreen}
options={{title: title('Log')}}
/>
<Stack.Screen
name="Support"
component={SupportScreen}
getComponent={() => SupportScreen}
options={{title: title('Support')}}
/>
<Stack.Screen
name="PrivacyPolicy"
component={PrivacyPolicyScreen}
getComponent={() => PrivacyPolicyScreen}
options={{title: title('Privacy Policy')}}
/>
<Stack.Screen
name="TermsOfService"
component={TermsOfServiceScreen}
getComponent={() => TermsOfServiceScreen}
options={{title: title('Terms of Service')}}
/>
<Stack.Screen
name="CommunityGuidelines"
component={CommunityGuidelinesScreen}
getComponent={() => CommunityGuidelinesScreen}
options={{title: title('Community Guidelines')}}
/>
<Stack.Screen
name="CopyrightPolicy"
component={CopyrightPolicyScreen}
getComponent={() => CopyrightPolicyScreen}
options={{title: title('Copyright Policy')}}
/>
<Stack.Screen
name="AppPasswords"
component={AppPasswords}
getComponent={() => AppPasswords}
options={{title: title('App Passwords')}}
/>
<Stack.Screen
name="SavedFeeds"
component={SavedFeeds}
getComponent={() => SavedFeeds}
options={{title: title('Edit My Feeds')}}
/>
<Stack.Screen
name="PreferencesHomeFeed"
getComponent={() => PreferencesHomeFeed}
options={{title: title('Home Feed Preferences')}}
/>
<Stack.Screen
name="PreferencesThreads"
getComponent={() => PreferencesThreads}
options={{title: title('Threads Preferences')}}
/>
</>
)
}
@@ -220,27 +241,37 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
* in 3 distinct tab-stacks with a different root screen on each.
*/
function TabsNavigator() {
const tabBar = React.useCallback(props => <BottomBar {...props} />, [])
const tabBar = React.useCallback(
(props: JSX.IntrinsicAttributes & BottomTabBarProps) => (
<BottomBar {...props} />
),
[],
)
return (
<Tab.Navigator
initialRouteName="HomeTab"
backBehavior="initialRoute"
screenOptions={{headerShown: false}}
screenOptions={{headerShown: false, lazy: true}}
tabBar={tabBar}>
<Tab.Screen name="HomeTab" component={HomeTabNavigator} />
<Tab.Screen name="SearchTab" component={SearchTabNavigator} />
<Tab.Screen name="FeedsTab" component={FeedsTabNavigator} />
<Tab.Screen name="HomeTab" getComponent={() => HomeTabNavigator} />
<Tab.Screen name="SearchTab" getComponent={() => SearchTabNavigator} />
<Tab.Screen name="FeedsTab" getComponent={() => FeedsTabNavigator} />
<Tab.Screen
name="NotificationsTab"
component={NotificationsTabNavigator}
getComponent={() => NotificationsTabNavigator}
/>
<Tab.Screen
name="MyProfileTab"
getComponent={() => MyProfileTabNavigator}
/>
<Tab.Screen name="MyProfileTab" component={MyProfileTabNavigator} />
</Tab.Navigator>
)
}
function HomeTabNavigator() {
const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark)
return (
<HomeTab.Navigator
screenOptions={{
@@ -250,7 +281,7 @@ function HomeTabNavigator() {
animationDuration: 250,
contentStyle,
}}>
<HomeTab.Screen name="Home" component={HomeScreen} />
<HomeTab.Screen name="Home" getComponent={() => HomeScreen} />
{commonScreens(HomeTab)}
</HomeTab.Navigator>
)
@@ -267,7 +298,7 @@ function SearchTabNavigator() {
animationDuration: 250,
contentStyle,
}}>
<SearchTab.Screen name="Search" component={SearchScreen} />
<SearchTab.Screen name="Search" getComponent={() => SearchScreen} />
{commonScreens(SearchTab as typeof HomeTab)}
</SearchTab.Navigator>
)
@@ -284,7 +315,7 @@ function FeedsTabNavigator() {
animationDuration: 250,
contentStyle,
}}>
<FeedsTab.Screen name="Feeds" component={FeedsScreen} />
<FeedsTab.Screen name="Feeds" getComponent={() => FeedsScreen} />
{commonScreens(FeedsTab as typeof HomeTab)}
</FeedsTab.Navigator>
)
@@ -303,14 +334,14 @@ function NotificationsTabNavigator() {
}}>
<NotificationsTab.Screen
name="Notifications"
component={NotificationsScreen}
getComponent={() => NotificationsScreen}
/>
{commonScreens(NotificationsTab as typeof HomeTab)}
</NotificationsTab.Navigator>
)
}
const MyProfileTabNavigator = observer(() => {
const MyProfileTabNavigator = observer(function MyProfileTabNavigatorImpl() {
const contentStyle = useColorSchemeStyle(styles.bgLight, styles.bgDark)
const store = useStores()
return (
@@ -325,10 +356,9 @@ const MyProfileTabNavigator = observer(() => {
<MyProfileTab.Screen
name="MyProfile"
// @ts-ignore // TODO: fix this broken type in ProfileScreen
component={ProfileScreen}
getComponent={() => ProfileScreen}
initialParams={{
name: store.me.did,
hideBackButton: true,
}}
/>
{commonScreens(MyProfileTab as typeof HomeTab)}
@@ -340,9 +370,11 @@ const MyProfileTabNavigator = observer(() => {
* The FlatNavigator is used by Web to represent the routes
* in a single ("flat") stack.
*/
const FlatNavigator = observer(() => {
const FlatNavigator = observer(function FlatNavigatorImpl() {
const pal = usePalette('default')
const unreadCountLabel = useStores().me.notifications.unreadCountLabel
const store = useStores()
const unreadCountLabel = store.me.notifications.unreadCountLabel
const title = (page: string) => bskyTitle(page, unreadCountLabel)
return (
<Flat.Navigator
@@ -355,22 +387,22 @@ const FlatNavigator = observer(() => {
}}>
<Flat.Screen
name="Home"
component={HomeScreen}
getComponent={() => HomeScreen}
options={{title: title('Home')}}
/>
<Flat.Screen
name="Search"
component={SearchScreen}
getComponent={() => SearchScreen}
options={{title: title('Search')}}
/>
<Flat.Screen
name="Feeds"
component={FeedsScreen}
getComponent={() => FeedsScreen}
options={{title: title('Feeds')}}
/>
<Flat.Screen
name="Notifications"
component={NotificationsScreen}
getComponent={() => NotificationsScreen}
options={{title: title('Notifications')}}
/>
{commonScreens(Flat as typeof HomeTab, unreadCountLabel)}
@@ -410,7 +442,16 @@ const LINKING = {
if (name === 'Notifications') {
return buildStateObject('NotificationsTab', 'Notifications', params)
}
return buildStateObject('HomeTab', name, params)
if (name === 'Home') {
return buildStateObject('HomeTab', 'Home', params)
}
// if the path is something else, like a post, profile, or even settings, we need to initialize the home tab as pre-existing state otherwise the back button will not work
return buildStateObject('HomeTab', name, params, [
{
name: 'Home',
params: {},
},
])
} else {
return buildStateObject('Flat', name, params)
}
@@ -425,6 +466,14 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
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()
@@ -446,9 +495,21 @@ function navigate<K extends keyof AllNavigatorParams>(
params?: AllNavigatorParams[K],
) {
if (navigationRef.isReady()) {
// @ts-ignore I dont know what would make typescript happy but I have a life -prf
navigationRef.navigate(name, params)
return Promise.race([
new Promise<void>(resolve => {
const handler = () => {
resolve()
navigationRef.removeListener('state', handler)
}
navigationRef.addListener('state', handler)
// @ts-ignore I dont know what would make typescript happy but I have a life -prf
navigationRef.navigate(name, params)
}),
timeout(1e3),
])
}
return Promise.resolve()
}
function resetToTab(tabName: 'HomeTab' | 'SearchTab' | 'NotificationsTab') {
@@ -460,7 +521,8 @@ function resetToTab(tabName: 'HomeTab' | 'SearchTab' | 'NotificationsTab') {
}
}
function reset() {
// returns a promise that resolves after the state reset is complete
function reset(): Promise<void> {
if (navigationRef.isReady()) {
navigationRef.dispatch(
CommonActions.reset({
@@ -468,6 +530,18 @@ function reset() {
routes: [{name: isNative ? 'HomeTab' : 'Home'}],
}),
)
return Promise.race([
timeout(1e3),
new Promise<void>(resolve => {
const handler = () => {
resolve()
navigationRef.removeListener('state', handler)
}
navigationRef.addListener('state', handler)
}),
])
} else {
return Promise.resolve()
}
}
@@ -513,6 +587,18 @@ const styles = StyleSheet.create({
},
})
function logModuleInitTrace() {
if (__DEV__) {
// This log is noisy, so keep false committed
const shouldLog = false
// Relies on our patch to polyfill.js in metro-runtime
const initLogs = (global as any).__INIT_LOGS__
if (shouldLog && Array.isArray(initLogs)) {
console.log(initLogs.join('\n'))
}
}
}
export {
navigate,
resetToTab,
+45 -9
View File
@@ -1,5 +1,12 @@
import React, {createContext, useContext, useMemo} from 'react'
import {TextStyle, useColorScheme, ViewStyle} from 'react-native'
import {isWeb} from 'platform/detection'
import React, {ReactNode, createContext, useContext} from 'react'
import {
AppState,
TextStyle,
useColorScheme as useColorScheme_BUGGY,
ViewStyle,
ColorSchemeName,
} from 'react-native'
import {darkTheme, defaultTheme} from './themes'
export type ColorScheme = 'light' | 'dark'
@@ -78,23 +85,52 @@ export interface Theme {
}
export interface ThemeProviderProps {
theme?: ColorScheme
children?: ReactNode
theme?: 'light' | 'dark' | 'system'
}
export const ThemeContext = createContext<Theme>(defaultTheme)
export const useTheme = () => useContext(ThemeContext)
function getTheme(theme: ColorSchemeName) {
return theme === 'dark' ? darkTheme : defaultTheme
}
/**
* With RN iOS, we can only "trust" the color scheme reported while the app is
* active. This is a workaround until the bug is fixed upstream.
*
* @see https://github.com/bluesky-social/social-app/pull/1417#issuecomment-1719868504
* @see https://github.com/facebook/react-native/pull/39439
*/
function useColorScheme_FIXED() {
const colorScheme = useColorScheme_BUGGY()
const [currentColorScheme, setCurrentColorScheme] =
React.useState<ColorSchemeName>(colorScheme)
React.useEffect(() => {
// we don't need to be updating state on web
if (isWeb) return
const subscription = AppState.addEventListener('change', state => {
const isActive = state === 'active'
if (!isActive) return
setCurrentColorScheme(colorScheme)
})
return () => subscription.remove()
}, [colorScheme])
return isWeb ? colorScheme : currentColorScheme
}
export const ThemeProvider: React.FC<ThemeProviderProps> = ({
theme,
children,
}) => {
const colorScheme = useColorScheme()
const colorScheme = useColorScheme_FIXED()
const themeValue = getTheme(theme === 'system' ? colorScheme : theme)
const value = useMemo(
() => ((theme || colorScheme) === 'dark' ? darkTheme : defaultTheme),
[colorScheme, theme],
return (
<ThemeContext.Provider value={themeValue}>{children}</ThemeContext.Provider>
)
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}
@@ -4,32 +4,45 @@ 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 {sha256} from 'js-sha256'
import {ScreenEvent, TrackEvent} from './types'
const segmentClient = createClient({
writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
trackAppLifecycleEvents: false,
proxy: 'https://api.events.bsky.app/v1',
})
export const track = segmentClient?.track?.bind?.(segmentClient) as TrackEvent
export function useAnalytics() {
const store = useStores()
const methods = useAnalyticsOrig()
const methods: ClientMethods = useAnalyticsOrig()
return React.useMemo(() => {
if (store.session.hasSession) {
return methods
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,
}
}
// dont send analytics pings for anonymous users
return {
screen: () => {},
track: () => {},
identify: () => {},
flush: () => {},
group: () => {},
alias: () => {},
reset: () => {},
screen: () => Promise<void>,
track: () => Promise<void>,
identify: () => Promise<void>,
flush: () => Promise<void>,
group: () => Promise<void>,
alias: () => Promise<void>,
reset: () => Promise<void>,
}
}, [store, methods])
}
@@ -38,10 +51,10 @@ export function init(store: RootStoreModel) {
store.onSessionLoaded(() => {
const sess = store.session.currentSession
if (sess) {
if (sess.email) {
if (sess.did) {
const did_hashed = sha256(sess.did)
segmentClient.identify(did_hashed, {did_hashed})
store.log.debug('Ping w/hash')
const email_hashed = sha256(sess.email)
segmentClient.identify(email_hashed, {email_hashed})
} else {
store.log.debug('Ping w/o hash')
segmentClient.identify()
@@ -15,7 +15,7 @@ const segmentClient = createClient(
{
integrations: {
'Segment.io': {
apiHost: 'api.evt.bsky.app/v1',
apiHost: 'api.events.bsky.app/v1',
},
},
},
@@ -46,10 +46,10 @@ export function init(store: RootStoreModel) {
store.onSessionLoaded(() => {
const sess = store.session.currentSession
if (sess) {
if (sess.email) {
if (sess.did) {
const did_hashed = sha256(sess.did)
segmentClient.identify(did_hashed, {did_hashed})
store.log.debug('Ping w/hash')
const email_hashed = sha256(sess.email)
segmentClient.identify(email_hashed, {email_hashed})
} else {
store.log.debug('Ping w/o hash')
segmentClient.identify()
+153
View File
@@ -0,0 +1,153 @@
export type TrackEvent = (
event: keyof TrackPropertiesMap,
properties?: TrackPropertiesMap[keyof TrackPropertiesMap],
) => Promise<void>
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
'Create Account': {} // CAN BE SERVER
'Try Create Account': {}
'Create Account Successfully': {}
'Signin:PressedForgotPassword': {}
'Signin:PressedSelectService': {}
// COMPOSER / CREATE POST events
'Create Post': {imageCount: string} // CAN BE SERVER
'Composer:PastedPhotos': {}
'Composer:CameraOpened': {}
'Composer:GalleryOpened': {}
'HomeScreen:PressCompose': {}
'ProfileScreen:PressCompose': {}
// EDIT PROFILE events
'EditHandle:ViewCustomForm': {}
'EditHandle:ViewProvidedForm': {}
'EditHandle:SetNewHandle': {}
'EditProfile:AvatarSelected': {}
'EditProfile:BannerSelected': {}
'EditProfile:Save': {} // CAN BE SERVER
// FEED events
'Feed:onRefresh': {}
'Feed:onEndReached': {}
// POST events
'Post:Like': {} // CAN BE SERVER
'Post:Unlike': {} // CAN BE SERVER
'Post:Repost': {} // CAN BE SERVER
'Post:Unrepost': {} // CAN BE SERVER
'Post:Delete': {} // CAN BE SERVER
'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
}
'Profile:Unfollow': {
username: string
}
// PROFILE HEADER events
'ProfileHeader:EditProfileButtonClicked': {}
'ProfileHeader:FollowersButtonClicked': {
handle: string
}
'ProfileHeader:FollowsButtonClicked': {
handle: string
}
'ProfileHeader:ShareButtonClicked': {}
'ProfileHeader:MuteAccountButtonClicked': {}
'ProfileHeader:UnmuteAccountButtonClicked': {}
'ProfileHeader:ReportAccountButtonClicked': {}
'ProfileHeader:AddToListsButtonClicked': {}
'ProfileHeader:BlockAccountButtonClicked': {}
'ProfileHeader:UnblockAccountButtonClicked': {}
'ProfileHeader:FollowButtonClicked': {}
'ProfileHeader:UnfollowButtonClicked': {}
'ProfileHeader:SuggestedFollowsOpened': {}
'ProfileHeader:SuggestedFollowFollowed': {}
'ViewHeader:MenuButtonClicked': {}
// SETTINGS events
'Settings:SwitchAccountButtonClicked': {}
'Settings:AddAccountButtonClicked': {}
'Settings:ChangeHandleButtonClicked': {}
'Settings:InvitecodesButtonClicked': {}
'Settings:ContentfilteringButtonClicked': {}
'Settings:SignOutButtonClicked': {}
'Settings:ContentlanguagesButtonClicked': {}
// MENU events
'Menu:ItemClicked': {url: string}
'Menu:FeedbackClicked': {}
'Menu:HelpClicked': {}
// MOBILE SHELL events
'MobileShell:MyProfileButtonPressed': {}
'MobileShell:HomeButtonPressed': {}
'MobileShell:SearchButtonPressed': {}
'MobileShell:NotificationsButtonPressed': {}
'MobileShell:FeedsButtonPressed': {}
// NOTIFICATIONS events
'Notificatons:OpenApp': {}
// LISTS events
'Lists:onRefresh': {}
'Lists:onEndReached': {}
'CreateMuteList:AvatarSelected': {}
'CreateMuteList:Save': {} // CAN BE SERVER
'Lists:Subscribe': {} // CAN BE SERVER
'Lists:Unsubscribe': {} // CAN BE SERVER
// CUSTOM FEED events
'CustomFeed:Save': {}
'CustomFeed:Unsave': {}
'CustomFeed:Like': {}
'CustomFeed:Unlike': {}
'CustomFeed:Share': {}
'CustomFeed:Pin': {
uri: string
name: string
}
'CustomFeed:Unpin': {
uri: string
name: string
}
'CustomFeed:Reorder': {
uri: string
name: string
index: number
}
'CustomFeed:LoadMore': {}
'MultiFeed:onEndReached': {}
'MultiFeed:onRefresh': {}
// MODERATION events
'Moderation:ContentfilteringButtonClicked': {}
// ONBOARDING events
'Onboarding:Begin': {}
'Onboarding:Complete': {}
'Onboarding:Skipped': {}
'Onboarding:Reset': {}
'Onboarding:SuggestedFollowFollowed': {}
}
interface ScreenPropertiesMap {
Login: {}
CreateAccount: {}
'Choose Account': {}
'Signin:ForgotPassword': {}
'Signin:SetNewPasswordForm': {}
'Signin:PasswordUpdatedForm': {}
Feed: {}
Notifications: {}
Profile: {}
'Profile:Preview': {}
Settings: {}
AppPasswords: {}
Moderation: {}
BlockedAccounts: {}
MutedAccounts: {}
SavedFeeds: {}
}
+1 -1
View File
@@ -11,7 +11,7 @@ export function doPolyfill() {
interface FetchHandlerResponse {
status: number
headers: Record<string, string>
body: ArrayBuffer | undefined
body: any
}
async function fetchHandler(
-137
View File
@@ -1,137 +0,0 @@
import {RootStoreModel} from 'state/index'
import {
AppBskyFeedDefs,
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
} from '@atproto/api'
type ReasonRepost = AppBskyFeedDefs.ReasonRepost
async function getMultipleAuthorsPosts(
rootStore: RootStoreModel,
authors: string[],
cursor: string | undefined = undefined,
limit: number = 10,
) {
const responses = await Promise.all(
authors.map((actor, index) =>
rootStore.agent
.getAuthorFeed({
actor,
limit,
cursor: cursor ? cursor.split(',')[index] : undefined,
})
.catch(_err => ({success: false, headers: {}, data: {feed: []}})),
),
)
return responses
}
function mergePosts(
responses: GetAuthorFeed.Response[],
{repostsOnly, bestOfOnly}: {repostsOnly?: boolean; bestOfOnly?: boolean},
) {
let posts: AppBskyFeedDefs.FeedViewPost[] = []
if (bestOfOnly) {
for (const res of responses) {
if (res.success) {
// filter the feed down to the post with the most likes
res.data.feed = res.data.feed.reduce(
(acc: AppBskyFeedDefs.FeedViewPost[], v) => {
if (
!acc?.[0] &&
!v.reason &&
!v.reply &&
isRecentEnough(v.post.indexedAt)
) {
return [v]
}
if (
acc &&
!v.reason &&
!v.reply &&
(v.post.likeCount || 0) > (acc[0]?.post.likeCount || 0) &&
isRecentEnough(v.post.indexedAt)
) {
return [v]
}
return acc
},
[],
)
}
}
}
// merge into one array
for (const res of responses) {
if (res.success) {
posts = posts.concat(res.data.feed)
}
}
// filter down to reposts of other users
const uris = new Set()
posts = posts.filter(p => {
if (repostsOnly && !isARepostOfSomeoneElse(p)) {
return false
}
if (uris.has(p.post.uri)) {
return false
}
uris.add(p.post.uri)
return true
})
// sort by index time
posts.sort((a, b) => {
return (
Number(new Date(b.post.indexedAt)) - Number(new Date(a.post.indexedAt))
)
})
return posts
}
function isARepostOfSomeoneElse(post: AppBskyFeedDefs.FeedViewPost): boolean {
return (
post.reason?.$type === 'app.bsky.feed.defs#reasonRepost' &&
post.post.author.did !== (post.reason as ReasonRepost).by.did
)
}
function getCombinedCursors(responses: GetAuthorFeed.Response[]) {
let hasCursor = false
const cursors = responses.map(r => {
if (r.data.cursor) {
hasCursor = true
return r.data.cursor
}
return ''
})
if (!hasCursor) {
return undefined
}
const combinedCursors = cursors.join(',')
return combinedCursors
}
function isCombinedCursor(cursor: string) {
return cursor.includes(',')
}
const TWO_DAYS_AGO = Date.now() - 1e3 * 60 * 60 * 48
function isRecentEnough(date: string) {
try {
const d = Number(new Date(date))
return d > TWO_DAYS_AGO
} catch {
return false
}
}
export {
getMultipleAuthorsPosts,
mergePosts,
getCombinedCursors,
isCombinedCursor,
}
+60
View File
@@ -0,0 +1,60 @@
/**
* APP-700
*
* This is a temporary debug setting we're running on the Web build to
* help the protocol team test some changes.
*
* It should be removed in ~2 weeks. It should only be used on the Web
* version of the app.
*/
import {useState, useCallback, useEffect} from 'react'
import {BskyAgent} from '@atproto/api'
import * as Storage from 'lib/storage'
export function useDebugHeaderSetting(agent: BskyAgent): [boolean, () => void] {
const [enabled, setEnabled] = useState<boolean>(false)
useEffect(() => {
async function check() {
if (await isEnabled()) {
setEnabled(true)
}
}
check()
}, [])
const toggle = useCallback(() => {
if (!enabled) {
Storage.saveString('set-header-x-appview-proxy', 'yes')
agent.api.xrpc.setHeader('x-appview-proxy', 'true')
setEnabled(true)
} else {
Storage.remove('set-header-x-appview-proxy')
agent.api.xrpc.unsetHeader('x-appview-proxy')
setEnabled(false)
}
}, [setEnabled, enabled, agent])
return [enabled, toggle]
}
export function setDebugHeader(agent: BskyAgent, enabled: boolean) {
if (enabled) {
Storage.saveString('set-header-x-appview-proxy', 'yes')
agent.api.xrpc.setHeader('x-appview-proxy', 'true')
} else {
Storage.remove('set-header-x-appview-proxy')
agent.api.xrpc.unsetHeader('x-appview-proxy')
}
}
export async function applyDebugHeader(agent: BskyAgent) {
if (await isEnabled()) {
agent.api.xrpc.setHeader('x-appview-proxy', 'true')
}
}
async function isEnabled() {
return (await Storage.loadString('set-header-x-appview-proxy')) === 'yes'
}
+172 -69
View File
@@ -1,7 +1,11 @@
import {AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
import lande from 'lande'
import {hasProp} from 'lib/type-guards'
import {LANGUAGES_MAP_CODE2} from '../../locale/languages'
import {
AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyEmbedRecordWithMedia,
AppBskyEmbedRecord,
} from '@atproto/api'
import {FeedSourceInfo} from './feed/types'
import {isPostInLanguage} from '../../locale/helpers'
type FeedViewPost = AppBskyFeedDefs.FeedViewPost
export type FeedTunerFn = (
@@ -14,6 +18,12 @@ export class FeedViewPostsSlice {
constructor(public items: FeedViewPost[] = []) {}
get _reactKey() {
return `slice-${this.items[0].post.uri}-${
this.items[0].reason?.indexedAt || this.items[0].post.indexedAt
}`
}
get uri() {
if (this.isFlattenedReply) {
return this.items[1].post.uri
@@ -55,6 +65,11 @@ export class FeedViewPostsSlice {
)
}
get source(): FeedSourceInfo | undefined {
return this.items.find(item => '__source' in item && !!item.__source)
?.__source as FeedSourceInfo
}
containsUri(uri: string) {
return !!this.items.find(item => item.post.uri === uri)
}
@@ -74,11 +89,31 @@ export class FeedViewPostsSlice {
}
flattenReplyParent() {
if (this.items[0].reply?.parent) {
this.isFlattenedReply = true
this.items.splice(0, 0, {post: this.items[0].reply?.parent})
if (this.items[0].reply) {
const reply = this.items[0].reply
if (AppBskyFeedDefs.isPostView(reply.parent)) {
this.isFlattenedReply = true
this.items.splice(0, 0, {post: reply.parent})
}
}
}
isFollowingAllAuthors(userDid: string) {
const item = this.rootItem
if (item.post.author.did === userDid) {
return true
}
if (AppBskyFeedDefs.isPostView(item.reply?.parent)) {
const parent = item.reply?.parent
if (parent?.author.did === userDid) {
return true
}
return (
parent?.author.viewer?.following && item.post.author.viewer?.following
)
}
return false
}
}
export class FeedTuner {
@@ -93,22 +128,32 @@ export class FeedTuner {
tune(
feed: FeedViewPost[],
tunerFns: FeedTunerFn[] = [],
{dryRun, maintainOrder}: {dryRun: boolean; maintainOrder: boolean} = {
dryRun: false,
maintainOrder: false,
},
): FeedViewPostsSlice[] {
let slices: FeedViewPostsSlice[] = []
// arrange the posts into thread slices
for (let i = feed.length - 1; i >= 0; i--) {
const item = feed[i]
if (maintainOrder) {
slices = feed.map(item => new FeedViewPostsSlice([item]))
} else {
// arrange the posts into thread slices
for (let i = feed.length - 1; i >= 0; i--) {
const item = feed[i]
const selfReplyUri = getSelfReplyUri(item)
if (selfReplyUri) {
const parent = slices.find(item2 => item2.isNextInThread(selfReplyUri))
if (parent) {
parent.insert(item)
continue
const selfReplyUri = getSelfReplyUri(item)
if (selfReplyUri) {
const parent = slices.find(item2 =>
item2.isNextInThread(selfReplyUri),
)
if (parent) {
parent.insert(item)
continue
}
}
slices.unshift(new FeedViewPostsSlice([item]))
}
slices.unshift(new FeedViewPostsSlice([item]))
}
// run the custom tuners
@@ -130,25 +175,60 @@ export class FeedTuner {
// turn non-threads with reply parents into threads
for (const slice of slices) {
if (!slice.isThread && !slice.items[0].reason && slice.items[0].reply) {
const reply = slice.items[0].reply
if (
AppBskyFeedDefs.isPostView(reply.parent) &&
!this.seenUris.has(reply.parent.uri) &&
!soonToBeSeenUris.has(reply.parent.uri)
) {
const uri = reply.parent.uri
slice.flattenReplyParent()
soonToBeSeenUris.add(uri)
}
}
}
if (!dryRun) {
for (const slice of slices) {
for (const item of slice.items) {
this.seenUris.add(item.post.uri)
}
}
}
return slices
}
static removeReplies(tuner: FeedTuner, slices: FeedViewPostsSlice[]) {
for (let i = slices.length - 1; i >= 0; i--) {
if (slices[i].isReply) {
slices.splice(i, 1)
}
}
return slices
}
static removeReposts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) {
for (let i = slices.length - 1; i >= 0; i--) {
const reason = slices[i].rootItem.reason
if (AppBskyFeedDefs.isReasonRepost(reason)) {
slices.splice(i, 1)
}
}
return slices
}
static removeQuotePosts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) {
for (let i = slices.length - 1; i >= 0; i--) {
const embed = slices[i].rootItem.post.embed
if (
!slice.isThread &&
!slice.items[0].reason &&
slice.items[0].reply?.parent &&
!this.seenUris.has(slice.items[0].reply?.parent.uri) &&
!soonToBeSeenUris.has(slice.items[0].reply?.parent.uri)
AppBskyEmbedRecord.isView(embed) ||
AppBskyEmbedRecordWithMedia.isView(embed)
) {
const uri = slice.items[0].reply?.parent.uri
slice.flattenReplyParent()
soonToBeSeenUris.add(uri)
slices.splice(i, 1)
}
}
for (const slice of slices) {
for (const item of slice.items) {
this.seenUris.add(item.post.uri)
}
}
return slices
}
@@ -174,53 +254,68 @@ export class FeedTuner {
return slices
}
static likedRepliesOnly(
tuner: FeedTuner,
slices: FeedViewPostsSlice[],
): FeedViewPostsSlice[] {
// remove any replies without at least 2 likes
for (let i = slices.length - 1; i >= 0; i--) {
if (slices[i].isFullThread || !slices[i].isReply) {
continue
}
const item = slices[i].rootItem
const isRepost = Boolean(item.reason)
if (!isRepost && (item.post.likeCount || 0) < 2) {
slices.splice(i, 1)
}
}
return slices
}
static preferredLangOnly(langsCode2: string[]) {
const langsCode3 = langsCode2.map(l => LANGUAGES_MAP_CODE2[l]?.code3 || l)
static thresholdRepliesOnly({
userDid,
minLikes,
followedOnly,
}: {
userDid: string
minLikes: number
followedOnly: boolean
}) {
return (
tuner: FeedTuner,
slices: FeedViewPostsSlice[],
): FeedViewPostsSlice[] => {
if (!langsCode2.length) {
// remove any replies without at least minLikes likes
for (let i = slices.length - 1; i >= 0; i--) {
const slice = slices[i]
if (slice.isFullThread || !slice.isReply) {
continue
}
const item = slice.rootItem
const isRepost = Boolean(item.reason)
if (isRepost) {
continue
}
if ((item.post.likeCount || 0) < minLikes) {
slices.splice(i, 1)
} else if (followedOnly && !slice.isFollowingAllAuthors(userDid)) {
slices.splice(i, 1)
}
}
return slices
}
}
/**
* This function filters a list of FeedViewPostsSlice items based on whether they contain text in a
* preferred language.
* @param {string[]} preferredLangsCode2 - An array of preferred language codes in ISO 639-1 or ISO 639-2 format.
* @returns A function that takes in a `FeedTuner` and an array of `FeedViewPostsSlice` objects and
* returns an array of `FeedViewPostsSlice` objects.
*/
static preferredLangOnly(preferredLangsCode2: string[]) {
return (
tuner: FeedTuner,
slices: FeedViewPostsSlice[],
): FeedViewPostsSlice[] => {
// early return if no languages have been specified
if (!preferredLangsCode2.length || preferredLangsCode2.length === 0) {
return slices
}
for (let i = slices.length - 1; i >= 0; i--) {
let hasPreferredLang = false
for (const item of slices[i].items) {
if (
hasProp(item.post.record, 'text') &&
typeof item.post.record.text === 'string'
) {
const res = lande(item.post.record.text)
if (langsCode3.includes(res[0][0])) {
hasPreferredLang = true
break
}
} else {
// no text? roll with it
if (isPostInLanguage(item.post, preferredLangsCode2)) {
hasPreferredLang = true
break
}
}
// if item does not fit preferred language, remove it
if (!hasPreferredLang) {
slices.splice(i, 1)
}
@@ -231,7 +326,15 @@ export class FeedTuner {
}
function getSelfReplyUri(item: FeedViewPost): string | undefined {
return item.reply?.parent.author.did === item.post.author.did
? item.reply?.parent.uri
: undefined
if (item.reply) {
if (
AppBskyFeedDefs.isPostView(item.reply.parent) &&
!AppBskyFeedDefs.isReasonRepost(item.reason) // don't thread reposted self-replies
) {
return item.reply.parent.author.did === item.post.author.did
? item.reply.parent.uri
: undefined
}
}
return undefined
}
+91
View File
@@ -0,0 +1,91 @@
import {
AppBskyFeedDefs,
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
} from '@atproto/api'
import {RootStoreModel} from 'state/index'
import {FeedAPI, FeedAPIResponse} from './types'
export class AuthorFeedAPI implements FeedAPI {
cursor: string | undefined
constructor(
public rootStore: RootStoreModel,
public params: GetAuthorFeed.QueryParams,
) {}
reset() {
this.cursor = undefined
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.rootStore.agent.getAuthorFeed({
...this.params,
limit: 1,
})
return res.data.feed[0]
}
async fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> {
const res = await this.rootStore.agent.getAuthorFeed({
...this.params,
cursor: this.cursor,
limit,
})
if (res.success) {
this.cursor = res.data.cursor
return {
cursor: res.data.cursor,
feed: this._filter(res.data.feed),
}
}
return {
feed: [],
}
}
_filter(feed: AppBskyFeedDefs.FeedViewPost[]) {
if (this.params.filter === 'posts_no_replies') {
return feed.filter(post => {
const isReply = post.reply
const isRepost = AppBskyFeedDefs.isReasonRepost(post.reason)
if (!isReply) return true
if (isRepost) return true
return isReply && isAuthorReplyChain(this.params.actor, post, feed)
})
}
return feed
}
}
function isAuthorReplyChain(
actor: string,
post: AppBskyFeedDefs.FeedViewPost,
posts: AppBskyFeedDefs.FeedViewPost[],
): boolean {
// current post is by a different user (shouldn't happen)
if (post.post.author.handle !== 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
// A top-level post that matches the parent of the current post.
const parentPost = posts.find(p => p.post.uri === replyParent.uri)
/*
* Either we haven't fetched the parent at the top level, or the only
* record we have is on feedItem.reply.parent, which we've already checked
* above.
*/
if (!parentPost) return true
// Walk up to parent
return isAuthorReplyChain(actor, parentPost, posts)
}
// Just default to showing it
return true
}
+52
View File
@@ -0,0 +1,52 @@
import {
AppBskyFeedDefs,
AppBskyFeedGetFeed as GetCustomFeed,
} from '@atproto/api'
import {RootStoreModel} from 'state/index'
import {FeedAPI, FeedAPIResponse} from './types'
export class CustomFeedAPI implements FeedAPI {
cursor: string | undefined
constructor(
public rootStore: RootStoreModel,
public params: GetCustomFeed.QueryParams,
) {}
reset() {
this.cursor = undefined
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.rootStore.agent.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({
...this.params,
cursor: this.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
// -prf
if (res.data.feed.length > limit) {
res.data.feed = res.data.feed.slice(0, limit)
}
return {
cursor: res.data.cursor,
feed: res.data.feed,
}
}
return {
feed: [],
}
}
}
+37
View File
@@ -0,0 +1,37 @@
import {AppBskyFeedDefs} from '@atproto/api'
import {RootStoreModel} from 'state/index'
import {FeedAPI, FeedAPIResponse} from './types'
export class FollowingFeedAPI implements FeedAPI {
cursor: string | undefined
constructor(public rootStore: RootStoreModel) {}
reset() {
this.cursor = undefined
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.rootStore.agent.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,
limit,
})
if (res.success) {
this.cursor = res.data.cursor
return {
cursor: res.data.cursor,
feed: res.data.feed,
}
}
return {
feed: [],
}
}
}
+45
View File
@@ -0,0 +1,45 @@
import {
AppBskyFeedDefs,
AppBskyFeedGetActorLikes as GetActorLikes,
} from '@atproto/api'
import {RootStoreModel} from 'state/index'
import {FeedAPI, FeedAPIResponse} from './types'
export class LikesFeedAPI implements FeedAPI {
cursor: string | undefined
constructor(
public rootStore: RootStoreModel,
public params: GetActorLikes.QueryParams,
) {}
reset() {
this.cursor = undefined
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.rootStore.agent.getActorLikes({
...this.params,
limit: 1,
})
return res.data.feed[0]
}
async fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> {
const res = await this.rootStore.agent.getActorLikes({
...this.params,
cursor: this.cursor,
limit,
})
if (res.success) {
this.cursor = res.data.cursor
return {
cursor: res.data.cursor,
feed: res.data.feed,
}
}
return {
feed: [],
}
}
}
+255
View File
@@ -0,0 +1,255 @@
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'
const REQUEST_WAIT_MS = 500 // 500ms
const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours
export class MergeFeedAPI implements FeedAPI {
following: MergeFeedSource_Following
customFeeds: MergeFeedSource_Custom[] = []
feedCursor = 0
itemCursor = 0
sampleCursor = 0
constructor(public rootStore: RootStoreModel) {
this.following = new MergeFeedSource_Following(this.rootStore)
}
reset() {
this.following = new MergeFeedSource_Following(this.rootStore)
this.customFeeds = [] // just empty the array, they will be captured in _fetchNext()
this.feedCursor = 0
this.itemCursor = 0
this.sampleCursor = 0
}
async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.rootStore.agent.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()
const promises = []
// always keep following topped up
if (this.following.numReady < limit) {
promises.push(this.following.fetchNext(60))
}
// pick the next feeds to sample from
const feeds = this.customFeeds.slice(this.feedCursor, this.feedCursor + 3)
this.feedCursor += 3
if (this.feedCursor > this.customFeeds.length) {
this.feedCursor = 0
}
// top up the feeds
for (const feed of feeds) {
if (feed.numReady < 5) {
promises.push(feed.fetchNext(10))
}
}
// wait for requests (all capped at a fixed timeout)
await Promise.all(promises)
// assemble a response by sampling from feeds with content
const posts: AppBskyFeedDefs.FeedViewPost[] = []
while (posts.length < limit) {
let slice = this.sampleItem()
if (slice[0]) {
posts.push(slice[0])
} else {
break
}
}
return {
cursor: posts.length ? 'fake' : undefined,
feed: posts,
}
}
sampleItem() {
const i = this.itemCursor++
const candidateFeeds = this.customFeeds.filter(f => f.numReady > 0)
const canSample = candidateFeeds.length > 0
const hasFollows = this.following.hasMore
const hasFollowsReady = this.following.numReady > 0
// 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)
if (!canSample && !hasFollows) {
// no data available
return []
}
if (shouldSample || !hasFollows) {
// time to sample, or the user isnt following anybody
return candidateFeeds[this.sampleCursor++ % candidateFeeds.length].take(1)
}
if (!hasFollowsReady) {
// stop here so more follows can be fetched
return []
}
// 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
cursor: string | undefined = undefined
queue: AppBskyFeedDefs.FeedViewPost[] = []
hasMore = true
constructor(public rootStore: RootStoreModel) {}
get numReady() {
return this.queue.length
}
get needsFetch() {
return this.hasMore && this.queue.length === 0
}
reset() {
this.cursor = undefined
this.queue = []
this.hasMore = true
}
take(n: number): AppBskyFeedDefs.FeedViewPost[] {
return this.queue.splice(0, n)
}
async fetchNext(n: number) {
await Promise.race([this._fetchNextInner(n), timeout(REQUEST_WAIT_MS)])
}
_fetchNextInner = bundleAsync(async (n: number) => {
const res = await this._getFeed(this.cursor, n)
if (res.success) {
this.cursor = res.data.cursor
if (res.data.feed.length) {
this.queue = this.queue.concat(res.data.feed)
} else {
this.hasMore = false
}
} else {
this.hasMore = false
}
})
protected _getFeed(
_cursor: string | undefined,
_limit: number,
): Promise<AppBskyFeedGetTimeline.Response> {
throw new Error('Must be overridden')
}
}
class MergeFeedSource_Following extends MergeFeedSource {
tuner = new FeedTuner()
reset() {
super.reset()
this.tuner.reset()
}
async fetchNext(n: number) {
return this._fetchNextInner(n)
}
protected async _getFeed(
cursor: string | undefined,
limit: number,
): Promise<AppBskyFeedGetTimeline.Response> {
const res = await this.rootStore.agent.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,
},
)
res.data.feed = slices.map(slice => slice.rootItem)
return res
}
}
class MergeFeedSource_Custom extends MergeFeedSource {
minDate: Date
constructor(
public rootStore: RootStoreModel,
public feedUri: string,
public feedDisplayName: string,
) {
super(rootStore)
this.sourceInfo = {
displayName: feedDisplayName,
uri: feedUriToHref(feedUri),
}
this.minDate = new Date(Date.now() - POST_AGE_CUTOFF)
}
protected async _getFeed(
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)
}
// 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
}
}
+17
View File
@@ -0,0 +1,17 @@
import {AppBskyFeedDefs} from '@atproto/api'
export interface FeedAPIResponse {
cursor?: string
feed: AppBskyFeedDefs.FeedViewPost[]
}
export interface FeedAPI {
reset(): void
peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost>
fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse>
}
export interface FeedSourceInfo {
uri: string
displayName: string
}
+24
View File
@@ -0,0 +1,24 @@
import {
AppBskyFeedDefs,
AppBskyFeedPost,
ComAtprotoRepoStrongRef,
} from '@atproto/api'
/**
* HACK
* The server doesnt seem to be correctly giving the notFound view yet
* so I'm adding it manually for now
* -prf
*/
export function hackAddDeletedEmbed(post: AppBskyFeedDefs.PostView) {
const record = post.record as AppBskyFeedPost.Record
if (record.embed?.$type === 'app.bsky.embed.record' && !post.embed) {
post.embed = {
$type: 'app.bsky.embed.record#view',
record: {
$type: 'app.bsky.embed.record#viewNotFound',
uri: (record.embed.record as ComAtprotoRepoStrongRef.Main).uri,
},
}
}
}
+49 -6
View File
@@ -4,6 +4,7 @@ import {
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyRichtextFacet,
ComAtprotoLabelDefs,
ComAtprotoRepoUploadBlob,
RichText,
} from '@atproto/api'
@@ -13,6 +14,7 @@ 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'
export interface ExternalEmbedDraft {
uri: string
@@ -29,10 +31,24 @@ export async function resolveName(store: RootStoreModel, didOrHandle: string) {
if (didOrHandle.startsWith('did:')) {
return didOrHandle
}
const res = await store.agent.resolveHandle({
handle: didOrHandle,
})
return res.data.did
// 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(
@@ -63,8 +79,10 @@ interface PostOpts {
}
extLink?: ExternalEmbedDraft
images?: ImageModel[]
labels?: string[]
knownHandles?: Set<string>
onStateChange?: (state: string) => void
langs?: string[]
}
export async function post(store: RootStoreModel, opts: PostOpts) {
@@ -75,8 +93,8 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
| AppBskyEmbedRecordWithMedia.Main
| undefined
let reply
const rt = new RichText(
{text: opts.rawText.trim()},
let rt = new RichText(
{text: opts.rawText.trimEnd()},
{
cleanNewlines: true,
},
@@ -84,6 +102,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
opts.onStateChange?.('Processing...')
await rt.detectFacets(store.agent)
rt = shortenLinks(rt)
// filter out any mention facets that didn't map to a user
rt.facets = rt.facets?.filter(facet => {
@@ -96,6 +115,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
return true
})
// add quote embed if present
if (opts.quote) {
embed = {
$type: 'app.bsky.embed.record',
@@ -106,15 +126,19 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
} as AppBskyEmbedRecord.Main
}
// add image embed if present
if (opts.images?.length) {
const images: AppBskyEmbedImages.Image[] = []
for (const image of opts.images) {
opts.onStateChange?.(`Uploading image #${images.length + 1}...`)
await image.compress()
const path = image.compressed?.path ?? image.path
const {width, height} = image.compressed || image
const res = await uploadBlob(store, path, 'image/jpeg')
images.push({
image: res.data.blob,
alt: image.altText ?? '',
aspectRatio: {width, height},
})
}
@@ -135,6 +159,7 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
}
}
// add external embed if present
if (opts.extLink && !opts.images?.length) {
if (opts.extLink.embed) {
embed = opts.extLink.embed
@@ -196,6 +221,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({
@@ -214,6 +240,21 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
}
}
// set labels
let labels: ComAtprotoLabelDefs.SelfLabels | undefined
if (opts.labels?.length) {
labels = {
$type: 'com.atproto.label.defs#selfLabels',
values: opts.labels.map(val => ({val})),
}
}
// add top 3 languages from user preferences if langs is provided
let langs = opts.langs
if (opts.langs) {
langs = opts.langs.slice(0, 3)
}
try {
opts.onStateChange?.('Posting...')
return await store.agent.post({
@@ -221,6 +262,8 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
facets: rt.facets,
reply,
embed,
langs,
labels,
})
} catch (e: any) {
console.error(`Failed to create post: ${e.toString()}`)
+3 -2
View File
@@ -1,4 +1,5 @@
import VersionNumber from 'react-native-version-number'
import * as Updates from 'expo-updates'
export const updateChannel = Updates.channel
export const appVersion = VersionNumber.appVersion
export const buildVersion = VersionNumber.buildVersion
export const appVersion = `${VersionNumber.appVersion} (${VersionNumber.buildVersion})`
+2 -3
View File
@@ -1,3 +1,2 @@
// TODO
export const appVersion = 'TODO'
export const buildVersion = 'TODO'
import {version} from '../../package.json'
export const appVersion = version
+3
View File
@@ -0,0 +1,3 @@
export function timeout(ms: number): Promise<void> {
return new Promise(r => setTimeout(r, ms))
}
-18
View File
@@ -1,18 +0,0 @@
import BackgroundFetch, {
BackgroundFetchStatus,
} from 'react-native-background-fetch'
export function configure(
handler: (taskId: string) => Promise<void>,
timeoutHandler: (taskId: string) => void,
): Promise<BackgroundFetchStatus> {
return BackgroundFetch.configure(
{minimumFetchInterval: 15},
handler,
timeoutHandler,
)
}
export function finish(taskId: string) {
return BackgroundFetch.finish(taskId)
}
-13
View File
@@ -1,13 +0,0 @@
type BackgroundFetchStatus = 0 | 1 | 2
export async function configure(
_handler: (taskId: string) => Promise<void>,
_timeoutHandler: (taskId: string) => Promise<void>,
): Promise<BackgroundFetchStatus> {
// TODO
return 0
}
export function finish(_taskId: string) {
// TODO
}

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